@buoy-gg/route-events 1.7.2
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 +654 -0
- package/lib/commonjs/RouteObserver.js +54 -0
- package/lib/commonjs/RouteParser.js +310 -0
- package/lib/commonjs/RouteTracker.js +39 -0
- package/lib/commonjs/components/NavigationStack.js +584 -0
- package/lib/commonjs/components/RouteEventDetailContent.js +492 -0
- package/lib/commonjs/components/RouteEventExpandedContent.js +187 -0
- package/lib/commonjs/components/RouteEventItemCompact.js +175 -0
- package/lib/commonjs/components/RouteEventsModalWithTabs.js +560 -0
- package/lib/commonjs/components/RouteEventsTimeline.js +82 -0
- package/lib/commonjs/components/RouteFilterViewV2.js +42 -0
- package/lib/commonjs/components/RoutesSitemap.js +948 -0
- package/lib/commonjs/expoRouterStore.js +104 -0
- package/lib/commonjs/index.js +99 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/preset.js +83 -0
- package/lib/commonjs/useNavigationStack.js +241 -0
- package/lib/commonjs/useRouteObserver.js +73 -0
- package/lib/commonjs/useRouteSitemap.js +234 -0
- package/lib/commonjs/utils/safeExpoRouter.js +129 -0
- package/lib/commonjs/utils/safeReactNavigation.js +104 -0
- package/lib/module/RouteObserver.js +49 -0
- package/lib/module/RouteParser.js +305 -0
- package/lib/module/RouteTracker.js +35 -0
- package/lib/module/components/NavigationStack.js +580 -0
- package/lib/module/components/RouteEventDetailContent.js +487 -0
- package/lib/module/components/RouteEventExpandedContent.js +183 -0
- package/lib/module/components/RouteEventItemCompact.js +171 -0
- package/lib/module/components/RouteEventsModalWithTabs.js +557 -0
- package/lib/module/components/RouteEventsTimeline.js +78 -0
- package/lib/module/components/RouteFilterViewV2.js +38 -0
- package/lib/module/components/RoutesSitemap.js +944 -0
- package/lib/module/expoRouterStore.js +98 -0
- package/lib/module/index.js +23 -0
- package/lib/module/preset.js +79 -0
- package/lib/module/useNavigationStack.js +238 -0
- package/lib/module/useRouteObserver.js +70 -0
- package/lib/module/useRouteSitemap.js +229 -0
- package/lib/module/utils/safeExpoRouter.js +120 -0
- package/lib/module/utils/safeReactNavigation.js +98 -0
- package/lib/typescript/RouteObserver.d.ts +37 -0
- package/lib/typescript/RouteObserver.d.ts.map +1 -0
- package/lib/typescript/RouteParser.d.ts +129 -0
- package/lib/typescript/RouteParser.d.ts.map +1 -0
- package/lib/typescript/RouteTracker.d.ts +29 -0
- package/lib/typescript/RouteTracker.d.ts.map +1 -0
- package/lib/typescript/components/NavigationStack.d.ts +11 -0
- package/lib/typescript/components/NavigationStack.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventDetailContent.d.ts +21 -0
- package/lib/typescript/components/RouteEventDetailContent.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventExpandedContent.d.ts +16 -0
- package/lib/typescript/components/RouteEventExpandedContent.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventItemCompact.d.ts +15 -0
- package/lib/typescript/components/RouteEventItemCompact.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventsModalWithTabs.d.ts +15 -0
- package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventsTimeline.d.ts +17 -0
- package/lib/typescript/components/RouteEventsTimeline.d.ts.map +1 -0
- package/lib/typescript/components/RouteFilterViewV2.d.ts +9 -0
- package/lib/typescript/components/RouteFilterViewV2.d.ts.map +1 -0
- package/lib/typescript/components/RoutesSitemap.d.ts +15 -0
- package/lib/typescript/components/RoutesSitemap.d.ts.map +1 -0
- package/lib/typescript/expoRouterStore.d.ts +28 -0
- package/lib/typescript/expoRouterStore.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +18 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts +76 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/lib/typescript/useNavigationStack.d.ts +48 -0
- package/lib/typescript/useNavigationStack.d.ts.map +1 -0
- package/lib/typescript/useRouteObserver.d.ts +27 -0
- package/lib/typescript/useRouteObserver.d.ts.map +1 -0
- package/lib/typescript/useRouteSitemap.d.ts +102 -0
- package/lib/typescript/useRouteSitemap.d.ts.map +1 -0
- package/lib/typescript/utils/safeExpoRouter.d.ts +13 -0
- package/lib/typescript/utils/safeExpoRouter.d.ts.map +1 -0
- package/lib/typescript/utils/safeReactNavigation.d.ts +10 -0
- package/lib/typescript/utils/safeReactNavigation.d.ts.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useRouteSitemap - React hooks for accessing parsed route information
|
|
5
|
+
*
|
|
6
|
+
* Provides access to the Expo Router route tree with parsing, filtering,
|
|
7
|
+
* and search capabilities.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useEffect, useMemo, useCallback } from "react";
|
|
11
|
+
import { RouteParser } from "./RouteParser";
|
|
12
|
+
import { getRouteNodeMetadata, loadRouteNode } from "./expoRouterStore";
|
|
13
|
+
|
|
14
|
+
// Type-only definition to avoid Metro resolution issues
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract all routes from the navigation state recursively
|
|
18
|
+
* This builds a RouteNode-like structure from React Navigation's state
|
|
19
|
+
*/
|
|
20
|
+
function buildRouteNodeFromNavigationState(state) {
|
|
21
|
+
if (!state || !state.routes) return null;
|
|
22
|
+
|
|
23
|
+
// Find the app directory structure from the navigation state
|
|
24
|
+
// The root route usually contains the file-based routing structure
|
|
25
|
+
const rootRoute = state.routes?.[0];
|
|
26
|
+
if (!rootRoute) return null;
|
|
27
|
+
|
|
28
|
+
// Build a simple route node structure
|
|
29
|
+
// This will work with the RouteParser
|
|
30
|
+
const routeNode = {
|
|
31
|
+
type: 'route',
|
|
32
|
+
route: '',
|
|
33
|
+
dynamic: null,
|
|
34
|
+
children: [],
|
|
35
|
+
contextKey: '_app'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Recursively collect all routes from the state
|
|
39
|
+
function collectRoutes(navState, parent, pathPrefix = '') {
|
|
40
|
+
if (!navState || !navState.routes) return;
|
|
41
|
+
navState.routes.forEach(route => {
|
|
42
|
+
const routeName = route.name;
|
|
43
|
+
|
|
44
|
+
// Skip internal routes
|
|
45
|
+
if (routeName.startsWith('__') || routeName.includes('_layout') || routeName.startsWith('+not-found')) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create a route node
|
|
50
|
+
const node = {
|
|
51
|
+
type: routeName === 'index' ? 'route' : 'route',
|
|
52
|
+
route: routeName === 'index' ? '' : routeName,
|
|
53
|
+
dynamic: routeName.includes('[') ? [routeName.match(/\[([^\]]+)\]/)?.[1] || ''] : null,
|
|
54
|
+
children: [],
|
|
55
|
+
contextKey: `app/${routeName}`
|
|
56
|
+
};
|
|
57
|
+
parent.children.push(node);
|
|
58
|
+
|
|
59
|
+
// If this route has nested state, recurse
|
|
60
|
+
if (route.state) {
|
|
61
|
+
collectRoutes(route.state, node, `${pathPrefix}/${routeName}`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
collectRoutes(state, routeNode);
|
|
66
|
+
return routeNode.children.length > 0 ? routeNode : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Hook Options & Return Types
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Main Hook
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Hook to access and parse Expo Router's route tree
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```tsx
|
|
82
|
+
* const { routes, groups, stats, filteredRoutes } = useRouteSitemap({
|
|
83
|
+
* searchQuery: 'pokemon',
|
|
84
|
+
* sortBy: 'path',
|
|
85
|
+
* autoRefresh: true
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function useRouteSitemap(options = {}) {
|
|
90
|
+
const {
|
|
91
|
+
searchQuery = "",
|
|
92
|
+
sortBy = "path",
|
|
93
|
+
autoRefresh = false,
|
|
94
|
+
refreshInterval = 1000
|
|
95
|
+
} = options;
|
|
96
|
+
const [routeTreeState, setRouteTreeState] = useState(() => {
|
|
97
|
+
const node = loadRouteNode();
|
|
98
|
+
const metadata = getRouteNodeMetadata();
|
|
99
|
+
return {
|
|
100
|
+
node,
|
|
101
|
+
version: 0,
|
|
102
|
+
lastUpdatedAt: metadata.lastLoadedAt,
|
|
103
|
+
source: metadata.source
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
const routeNode = routeTreeState.node;
|
|
107
|
+
const routeNodeVersion = routeTreeState.version;
|
|
108
|
+
const lastUpdatedAt = routeTreeState.lastUpdatedAt;
|
|
109
|
+
const source = routeTreeState.source;
|
|
110
|
+
const refresh = useCallback(() => {
|
|
111
|
+
setRouteTreeState(previous => {
|
|
112
|
+
const node = loadRouteNode();
|
|
113
|
+
const metadata = getRouteNodeMetadata();
|
|
114
|
+
return {
|
|
115
|
+
node,
|
|
116
|
+
version: previous.version + 1,
|
|
117
|
+
lastUpdatedAt: metadata.lastLoadedAt,
|
|
118
|
+
source: metadata.source
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
// When the route tree isn't available yet (e.g., Expo Router still mounting),
|
|
124
|
+
// poll until it becomes ready. Use a more aggressive retry since the store
|
|
125
|
+
// is a singleton that gets populated asynchronously.
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (routeNode) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
let retryCount = 0;
|
|
131
|
+
const maxRetries = 100; // 10 seconds max
|
|
132
|
+
|
|
133
|
+
const poll = () => {
|
|
134
|
+
retryCount++;
|
|
135
|
+
refresh();
|
|
136
|
+
if (retryCount < maxRetries) {
|
|
137
|
+
timeoutRef = setTimeout(poll, 100);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
let timeoutRef = setTimeout(poll, 100);
|
|
141
|
+
return () => clearTimeout(timeoutRef);
|
|
142
|
+
}, [routeNode, refresh]);
|
|
143
|
+
|
|
144
|
+
// Optional auto-refresh hook for callers that want periodic updates
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (!autoRefresh) return;
|
|
147
|
+
const interval = setInterval(refresh, refreshInterval);
|
|
148
|
+
return () => clearInterval(interval);
|
|
149
|
+
}, [autoRefresh, refreshInterval, refresh]);
|
|
150
|
+
const isLoaded = !!routeNode;
|
|
151
|
+
|
|
152
|
+
// Parse routes
|
|
153
|
+
const routes = useMemo(() => {
|
|
154
|
+
if (!routeNode) return [];
|
|
155
|
+
return RouteParser.parseRouteTree(routeNode);
|
|
156
|
+
}, [routeNode, routeNodeVersion]);
|
|
157
|
+
|
|
158
|
+
// Sort routes
|
|
159
|
+
const sortedRoutes = useMemo(() => {
|
|
160
|
+
return RouteParser.sortRoutes(routes, sortBy);
|
|
161
|
+
}, [routes, sortBy]);
|
|
162
|
+
|
|
163
|
+
// Filter routes by search query
|
|
164
|
+
const filteredRoutes = useMemo(() => {
|
|
165
|
+
if (!searchQuery) return sortedRoutes;
|
|
166
|
+
return RouteParser.filterRoutes(sortedRoutes, searchQuery);
|
|
167
|
+
}, [sortedRoutes, searchQuery]);
|
|
168
|
+
|
|
169
|
+
// Organize into groups
|
|
170
|
+
const groups = useMemo(() => {
|
|
171
|
+
return RouteParser.organizeRoutes(filteredRoutes);
|
|
172
|
+
}, [filteredRoutes]);
|
|
173
|
+
|
|
174
|
+
// Calculate stats
|
|
175
|
+
const stats = useMemo(() => {
|
|
176
|
+
return RouteParser.getRouteStats(routes);
|
|
177
|
+
}, [routes]);
|
|
178
|
+
|
|
179
|
+
// Helper functions
|
|
180
|
+
const findRoute = path => RouteParser.findRouteByPath(routes, path);
|
|
181
|
+
const getParents = path => RouteParser.getParentRoutes(routes, path);
|
|
182
|
+
return {
|
|
183
|
+
routes: sortedRoutes,
|
|
184
|
+
groups,
|
|
185
|
+
stats,
|
|
186
|
+
filteredRoutes,
|
|
187
|
+
isLoaded,
|
|
188
|
+
refresh,
|
|
189
|
+
findRoute,
|
|
190
|
+
getParents,
|
|
191
|
+
lastUpdatedAt,
|
|
192
|
+
source
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Helper Hooks
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get a specific route by path
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```tsx
|
|
205
|
+
* const pokemonRoute = useRoute('/pokemon/[id]');
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
export function useRoute(path) {
|
|
209
|
+
const {
|
|
210
|
+
findRoute
|
|
211
|
+
} = useRouteSitemap();
|
|
212
|
+
return useMemo(() => findRoute(path), [findRoute, path]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get all parent routes for a given path
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```tsx
|
|
220
|
+
* const parents = useParentRoutes('/pokemon/[id]');
|
|
221
|
+
* // Returns: [{ path: '/pokemon', ... }]
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
export function useParentRoutes(path) {
|
|
225
|
+
const {
|
|
226
|
+
getParents
|
|
227
|
+
} = useRouteSitemap();
|
|
228
|
+
return useMemo(() => getParents(path), [getParents, path]);
|
|
229
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safe wrapper for expo-router
|
|
5
|
+
*
|
|
6
|
+
* Provides optional imports for expo-router hooks and utilities.
|
|
7
|
+
* Falls back to no-op implementations when expo-router is not installed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let expoRouter = null;
|
|
11
|
+
let isAvailable = false;
|
|
12
|
+
let checkedAvailability = false;
|
|
13
|
+
function checkExpoRouterAvailability() {
|
|
14
|
+
if (checkedAvailability) return isAvailable;
|
|
15
|
+
try {
|
|
16
|
+
expoRouter = require("expo-router");
|
|
17
|
+
isAvailable = expoRouter != null;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
isAvailable = false;
|
|
20
|
+
expoRouter = null;
|
|
21
|
+
}
|
|
22
|
+
checkedAvailability = true;
|
|
23
|
+
return isAvailable;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// No-op implementations when expo-router is not available
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
function noOpUseRouter() {
|
|
31
|
+
return {
|
|
32
|
+
push: () => console.warn("[route-events] expo-router not installed: push() unavailable"),
|
|
33
|
+
replace: () => console.warn("[route-events] expo-router not installed: replace() unavailable"),
|
|
34
|
+
back: () => console.warn("[route-events] expo-router not installed: back() unavailable"),
|
|
35
|
+
canGoBack: () => false,
|
|
36
|
+
setParams: () => console.warn("[route-events] expo-router not installed: setParams() unavailable"),
|
|
37
|
+
navigate: () => console.warn("[route-events] expo-router not installed: navigate() unavailable")
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function noOpUsePathname() {
|
|
41
|
+
return "/";
|
|
42
|
+
}
|
|
43
|
+
function noOpUseSegments() {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
function noOpUseGlobalSearchParams() {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Safe hook exports
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
export function useSafeRouter() {
|
|
55
|
+
if (!checkExpoRouterAvailability()) {
|
|
56
|
+
return noOpUseRouter();
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return expoRouter.useRouter();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.warn("[route-events] Failed to use expo-router.useRouter:", error);
|
|
62
|
+
return noOpUseRouter();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function useSafePathname() {
|
|
66
|
+
if (!checkExpoRouterAvailability()) {
|
|
67
|
+
return noOpUsePathname();
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return expoRouter.usePathname();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.warn("[route-events] Failed to use expo-router.usePathname:", error);
|
|
73
|
+
return noOpUsePathname();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function useSafeSegments() {
|
|
77
|
+
if (!checkExpoRouterAvailability()) {
|
|
78
|
+
return noOpUseSegments();
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return expoRouter.useSegments();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.warn("[route-events] Failed to use expo-router.useSegments:", error);
|
|
84
|
+
return noOpUseSegments();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function useSafeGlobalSearchParams() {
|
|
88
|
+
if (!checkExpoRouterAvailability()) {
|
|
89
|
+
return noOpUseGlobalSearchParams();
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return expoRouter.useGlobalSearchParams();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.warn("[route-events] Failed to use expo-router.useGlobalSearchParams:", error);
|
|
95
|
+
return noOpUseGlobalSearchParams();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Router instance getter (for imperative navigation)
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
export function getSafeRouter() {
|
|
104
|
+
if (!checkExpoRouterAvailability()) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return expoRouter.router || null;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// Availability check
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
export function isExpoRouterAvailable() {
|
|
119
|
+
return checkExpoRouterAvailability();
|
|
120
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safe wrapper for @react-navigation/native
|
|
5
|
+
*
|
|
6
|
+
* Provides optional imports for React Navigation hooks.
|
|
7
|
+
* Falls back to no-op implementations when @react-navigation/native is not installed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useMemo } from "react";
|
|
11
|
+
let reactNavigation = null;
|
|
12
|
+
let isAvailable = false;
|
|
13
|
+
let checkedAvailability = false;
|
|
14
|
+
function checkReactNavigationAvailability() {
|
|
15
|
+
if (checkedAvailability) return isAvailable;
|
|
16
|
+
try {
|
|
17
|
+
reactNavigation = require("@react-navigation/native");
|
|
18
|
+
isAvailable = reactNavigation != null;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
isAvailable = false;
|
|
21
|
+
reactNavigation = null;
|
|
22
|
+
}
|
|
23
|
+
checkedAvailability = true;
|
|
24
|
+
return isAvailable;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// No-op implementations when @react-navigation/native is not available
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
function noOpUseNavigation() {
|
|
32
|
+
return useMemo(() => ({
|
|
33
|
+
navigate: () => console.warn("[route-events] @react-navigation/native not installed: navigate() unavailable"),
|
|
34
|
+
goBack: () => console.warn("[route-events] @react-navigation/native not installed: goBack() unavailable"),
|
|
35
|
+
canGoBack: () => false,
|
|
36
|
+
reset: () => console.warn("[route-events] @react-navigation/native not installed: reset() unavailable"),
|
|
37
|
+
setParams: () => console.warn("[route-events] @react-navigation/native not installed: setParams() unavailable"),
|
|
38
|
+
dispatch: () => console.warn("[route-events] @react-navigation/native not installed: dispatch() unavailable"),
|
|
39
|
+
isFocused: () => true,
|
|
40
|
+
addListener: () => () => {},
|
|
41
|
+
removeListener: () => {},
|
|
42
|
+
getParent: () => undefined,
|
|
43
|
+
getState: () => undefined,
|
|
44
|
+
getId: () => undefined
|
|
45
|
+
}), []);
|
|
46
|
+
}
|
|
47
|
+
function noOpUseNavigationState(selector) {
|
|
48
|
+
return useMemo(() => {
|
|
49
|
+
// Return a minimal state structure
|
|
50
|
+
const emptyState = {
|
|
51
|
+
key: "default",
|
|
52
|
+
index: 0,
|
|
53
|
+
routeNames: [],
|
|
54
|
+
routes: [],
|
|
55
|
+
type: "stack"
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
return selector ? selector(emptyState) : emptyState;
|
|
59
|
+
} catch {
|
|
60
|
+
return emptyState;
|
|
61
|
+
}
|
|
62
|
+
}, [selector]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Safe hook exports
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
export function useSafeNavigation() {
|
|
70
|
+
if (!checkReactNavigationAvailability()) {
|
|
71
|
+
return noOpUseNavigation();
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return reactNavigation.useNavigation();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.warn("[route-events] Failed to use @react-navigation/native.useNavigation:", error);
|
|
77
|
+
return noOpUseNavigation();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function useSafeNavigationState(selector) {
|
|
81
|
+
if (!checkReactNavigationAvailability()) {
|
|
82
|
+
return noOpUseNavigationState(selector);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return reactNavigation.useNavigationState(selector);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.warn("[route-events] Failed to use @react-navigation/native.useNavigationState:", error);
|
|
88
|
+
return noOpUseNavigationState(selector);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Availability check
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
export function isReactNavigationAvailable() {
|
|
97
|
+
return checkReactNavigationAvailability();
|
|
98
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RouteObserver - Tracks route changes in Expo Router
|
|
3
|
+
*
|
|
4
|
+
* Note: This is a simple event emitter that works with the useRouteObserver hook
|
|
5
|
+
* The actual route tracking happens in the hook using public Expo Router APIs
|
|
6
|
+
*/
|
|
7
|
+
export interface RouteChangeEvent {
|
|
8
|
+
pathname: string;
|
|
9
|
+
params: Record<string, string | string[]>;
|
|
10
|
+
segments: string[];
|
|
11
|
+
timestamp: number;
|
|
12
|
+
previousPathname?: string;
|
|
13
|
+
timeSincePrevious?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare class RouteObserver {
|
|
16
|
+
private listeners;
|
|
17
|
+
/**
|
|
18
|
+
* Emit a route change event
|
|
19
|
+
* Called by the useRouteObserver hook
|
|
20
|
+
*/
|
|
21
|
+
emit(event: RouteChangeEvent): void;
|
|
22
|
+
/**
|
|
23
|
+
* Add listener for route changes
|
|
24
|
+
* @returns Cleanup function to remove the listener
|
|
25
|
+
*/
|
|
26
|
+
addListener(callback: (event: RouteChangeEvent) => void): () => boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Remove a specific listener
|
|
29
|
+
*/
|
|
30
|
+
removeListener(callback: (event: RouteChangeEvent) => void): void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Singleton instance of RouteObserver
|
|
34
|
+
* Use this for all route tracking to ensure events are centralized
|
|
35
|
+
*/
|
|
36
|
+
export declare const routeObserver: RouteObserver;
|
|
37
|
+
//# sourceMappingURL=RouteObserver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouteObserver.d.ts","sourceRoot":"","sources":["../../src/RouteObserver.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,SAAS,CAAqD;IAEtE;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,gBAAgB;IAW5B;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI;IAKvD;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI;CAG3D;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,eAAsB,CAAC"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RouteParser - Extract and organize routes from Expo Router's RouteNode tree
|
|
3
|
+
*
|
|
4
|
+
* Based on research findings in:
|
|
5
|
+
* - docs/routing/expo/ROUTENODE_TYPE_DEFINITION.md
|
|
6
|
+
* - docs/routing/expo/ROUTES_SITEMAP_RESEARCH.md
|
|
7
|
+
*/
|
|
8
|
+
type RouteNode = {
|
|
9
|
+
type: 'route' | 'api' | 'layout' | 'redirect' | 'rewrite';
|
|
10
|
+
route: string;
|
|
11
|
+
contextKey: string;
|
|
12
|
+
children: RouteNode[];
|
|
13
|
+
dynamic: null | Array<{
|
|
14
|
+
name: string;
|
|
15
|
+
deep: boolean;
|
|
16
|
+
notFound?: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
internal?: boolean;
|
|
19
|
+
generated?: boolean;
|
|
20
|
+
initialRouteName?: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Route type classification
|
|
24
|
+
*/
|
|
25
|
+
export type RouteType = 'static' | 'dynamic' | 'catch-all' | 'index' | 'layout' | 'group' | 'not-found';
|
|
26
|
+
/**
|
|
27
|
+
* Parsed route information
|
|
28
|
+
*/
|
|
29
|
+
export interface RouteInfo {
|
|
30
|
+
/** Full path (e.g., "/pokemon/[id]") */
|
|
31
|
+
path: string;
|
|
32
|
+
/** Route name/segment (e.g., "[id]") */
|
|
33
|
+
name: string;
|
|
34
|
+
/** Route type classification */
|
|
35
|
+
type: RouteType;
|
|
36
|
+
/** Dynamic parameter names (e.g., ["id"]) */
|
|
37
|
+
params: string[];
|
|
38
|
+
/** Original RouteNode type */
|
|
39
|
+
nodeType: RouteNode['type'];
|
|
40
|
+
/** File path/context key */
|
|
41
|
+
contextKey: string;
|
|
42
|
+
/** Is this route internal/generated? */
|
|
43
|
+
isInternal: boolean;
|
|
44
|
+
/** Children routes */
|
|
45
|
+
children: RouteInfo[];
|
|
46
|
+
/** Depth in route tree (0 = root) */
|
|
47
|
+
depth: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Grouped routes for display
|
|
51
|
+
*/
|
|
52
|
+
export interface RouteGroup {
|
|
53
|
+
title: string;
|
|
54
|
+
icon: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
routes: RouteInfo[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Route statistics
|
|
60
|
+
*/
|
|
61
|
+
export interface RouteStats {
|
|
62
|
+
total: number;
|
|
63
|
+
static: number;
|
|
64
|
+
dynamic: number;
|
|
65
|
+
catchAll: number;
|
|
66
|
+
layouts: number;
|
|
67
|
+
groups: number;
|
|
68
|
+
}
|
|
69
|
+
export declare class RouteParser {
|
|
70
|
+
/**
|
|
71
|
+
* Parse RouteNode tree and extract all routes
|
|
72
|
+
*/
|
|
73
|
+
static parseRouteTree(rootNode: RouteNode | null): RouteInfo[];
|
|
74
|
+
/**
|
|
75
|
+
* Recursively traverse RouteNode tree
|
|
76
|
+
*/
|
|
77
|
+
private static traverseNode;
|
|
78
|
+
/**
|
|
79
|
+
* Build full path for a route node
|
|
80
|
+
*/
|
|
81
|
+
private static buildPath;
|
|
82
|
+
/**
|
|
83
|
+
* Detect the route type classification
|
|
84
|
+
*/
|
|
85
|
+
private static detectRouteType;
|
|
86
|
+
/**
|
|
87
|
+
* Extract dynamic parameter names from route
|
|
88
|
+
*/
|
|
89
|
+
private static extractParams;
|
|
90
|
+
/**
|
|
91
|
+
* Determine if route should be included in results
|
|
92
|
+
*/
|
|
93
|
+
private static shouldIncludeRoute;
|
|
94
|
+
/**
|
|
95
|
+
* Organize routes into groups for display
|
|
96
|
+
*/
|
|
97
|
+
static organizeRoutes(routes: RouteInfo[]): RouteGroup[];
|
|
98
|
+
/**
|
|
99
|
+
* Flatten nested routes into a single array
|
|
100
|
+
*/
|
|
101
|
+
private static flattenRoutes;
|
|
102
|
+
/**
|
|
103
|
+
* Get route statistics
|
|
104
|
+
*/
|
|
105
|
+
static getRouteStats(routes: RouteInfo[]): RouteStats;
|
|
106
|
+
/**
|
|
107
|
+
* Search/filter routes by query
|
|
108
|
+
*/
|
|
109
|
+
static filterRoutes(routes: RouteInfo[], query: string): RouteInfo[];
|
|
110
|
+
/**
|
|
111
|
+
* Build a visual tree string representation
|
|
112
|
+
*/
|
|
113
|
+
static buildTreeString(routes: RouteInfo[], depth?: number): string;
|
|
114
|
+
private static buildTreePrefix;
|
|
115
|
+
/**
|
|
116
|
+
* Sort routes by various criteria
|
|
117
|
+
*/
|
|
118
|
+
static sortRoutes(routes: RouteInfo[], sortBy?: 'path' | 'type' | 'name'): RouteInfo[];
|
|
119
|
+
/**
|
|
120
|
+
* Get route by path
|
|
121
|
+
*/
|
|
122
|
+
static findRouteByPath(routes: RouteInfo[], path: string): RouteInfo | null;
|
|
123
|
+
/**
|
|
124
|
+
* Get all parent routes for a given route
|
|
125
|
+
*/
|
|
126
|
+
static getParentRoutes(routes: RouteInfo[], targetPath: string): RouteInfo[];
|
|
127
|
+
}
|
|
128
|
+
export {};
|
|
129
|
+
//# sourceMappingURL=RouteParser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouteParser.d.ts","sourceRoot":"","sources":["../../src/RouteParser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RouteTracker - A component to place inside your navigation tree
|
|
3
|
+
*
|
|
4
|
+
* This component calls useRouteObserver() which uses expo-router hooks
|
|
5
|
+
* (usePathname, useSegments, etc.) to track navigation changes.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: This component MUST be placed inside your navigation tree
|
|
8
|
+
* (as a child of Stack, Tabs, or Slot) for route tracking to work.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* // In your _layout.tsx
|
|
13
|
+
* import { RouteTracker } from '@buoy-gg/route-events';
|
|
14
|
+
*
|
|
15
|
+
* export default function RootLayout() {
|
|
16
|
+
* return (
|
|
17
|
+
* <>
|
|
18
|
+
* <Stack>
|
|
19
|
+
* <Stack.Screen name="(tabs)" />
|
|
20
|
+
* </Stack>
|
|
21
|
+
* <RouteTracker />
|
|
22
|
+
* <FloatingDevTools ... />
|
|
23
|
+
* </>
|
|
24
|
+
* );
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function RouteTracker(): null;
|
|
29
|
+
//# sourceMappingURL=RouteTracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouteTracker.d.ts","sourceRoot":"","sources":["../../src/RouteTracker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,wBAAgB,YAAY,IAAI,IAAI,CAGnC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NavigationStack - Visual representation of current navigation stack
|
|
3
|
+
*
|
|
4
|
+
* Shows all screens currently mounted in memory, which one is visible,
|
|
5
|
+
* and provides controls to manipulate the stack.
|
|
6
|
+
*/
|
|
7
|
+
export interface NavigationStackProps {
|
|
8
|
+
style?: any;
|
|
9
|
+
}
|
|
10
|
+
export declare function NavigationStack({ style }: NavigationStackProps): import("react").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=NavigationStack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NavigationStack.d.ts","sourceRoot":"","sources":["../../../src/components/NavigationStack.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoDH,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,GAAG,CAAC;CACb;AAMD,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,EAAE,oBAAoB,+BAmZ9D"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RouteChangeEvent } from "../RouteObserver";
|
|
2
|
+
interface RouteConversation {
|
|
3
|
+
pathname: string;
|
|
4
|
+
lastEvent: RouteChangeEvent;
|
|
5
|
+
events: RouteChangeEvent[];
|
|
6
|
+
totalNavigations: number;
|
|
7
|
+
}
|
|
8
|
+
interface RouteEventDetailContentProps {
|
|
9
|
+
conversation: RouteConversation;
|
|
10
|
+
selectedEventIndex?: number;
|
|
11
|
+
onEventIndexChange?: (index: number) => void;
|
|
12
|
+
disableInternalFooter?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function RouteEventDetailContent({ conversation, selectedEventIndex, onEventIndexChange, disableInternalFooter, }: RouteEventDetailContentProps): import("react").JSX.Element;
|
|
15
|
+
export declare function RouteEventDetailFooter({ conversation, selectedEventIndex, onEventIndexChange, }: {
|
|
16
|
+
conversation: RouteConversation;
|
|
17
|
+
selectedEventIndex?: number;
|
|
18
|
+
onEventIndexChange?: (index: number) => void;
|
|
19
|
+
}): import("react").JSX.Element | null;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=RouteEventDetailContent.d.ts.map
|