@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.
Files changed (79) hide show
  1. package/README.md +654 -0
  2. package/lib/commonjs/RouteObserver.js +54 -0
  3. package/lib/commonjs/RouteParser.js +310 -0
  4. package/lib/commonjs/RouteTracker.js +39 -0
  5. package/lib/commonjs/components/NavigationStack.js +584 -0
  6. package/lib/commonjs/components/RouteEventDetailContent.js +492 -0
  7. package/lib/commonjs/components/RouteEventExpandedContent.js +187 -0
  8. package/lib/commonjs/components/RouteEventItemCompact.js +175 -0
  9. package/lib/commonjs/components/RouteEventsModalWithTabs.js +560 -0
  10. package/lib/commonjs/components/RouteEventsTimeline.js +82 -0
  11. package/lib/commonjs/components/RouteFilterViewV2.js +42 -0
  12. package/lib/commonjs/components/RoutesSitemap.js +948 -0
  13. package/lib/commonjs/expoRouterStore.js +104 -0
  14. package/lib/commonjs/index.js +99 -0
  15. package/lib/commonjs/package.json +1 -0
  16. package/lib/commonjs/preset.js +83 -0
  17. package/lib/commonjs/useNavigationStack.js +241 -0
  18. package/lib/commonjs/useRouteObserver.js +73 -0
  19. package/lib/commonjs/useRouteSitemap.js +234 -0
  20. package/lib/commonjs/utils/safeExpoRouter.js +129 -0
  21. package/lib/commonjs/utils/safeReactNavigation.js +104 -0
  22. package/lib/module/RouteObserver.js +49 -0
  23. package/lib/module/RouteParser.js +305 -0
  24. package/lib/module/RouteTracker.js +35 -0
  25. package/lib/module/components/NavigationStack.js +580 -0
  26. package/lib/module/components/RouteEventDetailContent.js +487 -0
  27. package/lib/module/components/RouteEventExpandedContent.js +183 -0
  28. package/lib/module/components/RouteEventItemCompact.js +171 -0
  29. package/lib/module/components/RouteEventsModalWithTabs.js +557 -0
  30. package/lib/module/components/RouteEventsTimeline.js +78 -0
  31. package/lib/module/components/RouteFilterViewV2.js +38 -0
  32. package/lib/module/components/RoutesSitemap.js +944 -0
  33. package/lib/module/expoRouterStore.js +98 -0
  34. package/lib/module/index.js +23 -0
  35. package/lib/module/preset.js +79 -0
  36. package/lib/module/useNavigationStack.js +238 -0
  37. package/lib/module/useRouteObserver.js +70 -0
  38. package/lib/module/useRouteSitemap.js +229 -0
  39. package/lib/module/utils/safeExpoRouter.js +120 -0
  40. package/lib/module/utils/safeReactNavigation.js +98 -0
  41. package/lib/typescript/RouteObserver.d.ts +37 -0
  42. package/lib/typescript/RouteObserver.d.ts.map +1 -0
  43. package/lib/typescript/RouteParser.d.ts +129 -0
  44. package/lib/typescript/RouteParser.d.ts.map +1 -0
  45. package/lib/typescript/RouteTracker.d.ts +29 -0
  46. package/lib/typescript/RouteTracker.d.ts.map +1 -0
  47. package/lib/typescript/components/NavigationStack.d.ts +11 -0
  48. package/lib/typescript/components/NavigationStack.d.ts.map +1 -0
  49. package/lib/typescript/components/RouteEventDetailContent.d.ts +21 -0
  50. package/lib/typescript/components/RouteEventDetailContent.d.ts.map +1 -0
  51. package/lib/typescript/components/RouteEventExpandedContent.d.ts +16 -0
  52. package/lib/typescript/components/RouteEventExpandedContent.d.ts.map +1 -0
  53. package/lib/typescript/components/RouteEventItemCompact.d.ts +15 -0
  54. package/lib/typescript/components/RouteEventItemCompact.d.ts.map +1 -0
  55. package/lib/typescript/components/RouteEventsModalWithTabs.d.ts +15 -0
  56. package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +1 -0
  57. package/lib/typescript/components/RouteEventsTimeline.d.ts +17 -0
  58. package/lib/typescript/components/RouteEventsTimeline.d.ts.map +1 -0
  59. package/lib/typescript/components/RouteFilterViewV2.d.ts +9 -0
  60. package/lib/typescript/components/RouteFilterViewV2.d.ts.map +1 -0
  61. package/lib/typescript/components/RoutesSitemap.d.ts +15 -0
  62. package/lib/typescript/components/RoutesSitemap.d.ts.map +1 -0
  63. package/lib/typescript/expoRouterStore.d.ts +28 -0
  64. package/lib/typescript/expoRouterStore.d.ts.map +1 -0
  65. package/lib/typescript/index.d.ts +18 -0
  66. package/lib/typescript/index.d.ts.map +1 -0
  67. package/lib/typescript/preset.d.ts +76 -0
  68. package/lib/typescript/preset.d.ts.map +1 -0
  69. package/lib/typescript/useNavigationStack.d.ts +48 -0
  70. package/lib/typescript/useNavigationStack.d.ts.map +1 -0
  71. package/lib/typescript/useRouteObserver.d.ts +27 -0
  72. package/lib/typescript/useRouteObserver.d.ts.map +1 -0
  73. package/lib/typescript/useRouteSitemap.d.ts +102 -0
  74. package/lib/typescript/useRouteSitemap.d.ts.map +1 -0
  75. package/lib/typescript/utils/safeExpoRouter.d.ts +13 -0
  76. package/lib/typescript/utils/safeExpoRouter.d.ts.map +1 -0
  77. package/lib/typescript/utils/safeReactNavigation.d.ts +10 -0
  78. package/lib/typescript/utils/safeReactNavigation.d.ts.map +1 -0
  79. package/package.json +72 -0
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+
3
+ let cachedStore = null;
4
+ let cachedStoreSource = null;
5
+ let importError = null;
6
+ let hasLoggedMissingStore = false;
7
+ let hasLoggedMissingRouteNode = false;
8
+ let lastRouteNodeTimestamp = null;
9
+ function logOnce(message, error) {
10
+ if (__DEV__) {
11
+ if (error) {
12
+ console.error(`[RouteEvents] ${message}`, error);
13
+ } else {
14
+ console.warn(`[RouteEvents] ${message}`);
15
+ }
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Attempt to require the expo-router store from known locations.
21
+ * Returns null (with a logged error in dev) when expo-router is not available.
22
+ *
23
+ * Note: The store uses getters that read from an internal storeRef.
24
+ * The storeRef gets populated when Expo Router's useStore() hook runs.
25
+ * So we cache the store reference but its property values update over time.
26
+ */
27
+ export function getExpoRouterStore() {
28
+ if (cachedStore) {
29
+ return cachedStore;
30
+ }
31
+ const loadFromBuild = () => {
32
+ try {
33
+ const module = require("expo-router/build/global-state/router-store");
34
+ if (module?.store) {
35
+ cachedStore = module.store;
36
+ cachedStoreSource = "build";
37
+ importError = null;
38
+ hasLoggedMissingStore = false;
39
+ return true;
40
+ }
41
+ } catch (error) {
42
+ importError = error;
43
+ }
44
+ return false;
45
+ };
46
+ const loadFromSrc = () => {
47
+ try {
48
+ const module = require("expo-router/src/global-state/router-store");
49
+ if (module?.store) {
50
+ cachedStore = module.store;
51
+ cachedStoreSource = "src";
52
+ importError = null;
53
+ hasLoggedMissingStore = false;
54
+ return true;
55
+ }
56
+ } catch (error) {
57
+ importError = error;
58
+ }
59
+ return false;
60
+ };
61
+ if (loadFromBuild() || loadFromSrc()) {
62
+ return cachedStore;
63
+ }
64
+ if (!hasLoggedMissingStore) {
65
+ logOnce("Unable to load expo-router internals. @buoy-gg/route-events requires expo-router >= 2.0.0. Install expo-router and ensure it is configured before using the Routes tab.", importError ?? undefined);
66
+ hasLoggedMissingStore = true;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ /**
72
+ * Returns the current RouteNode tree (if available).
73
+ * Logs a helpful warning in development when the tree is missing even though
74
+ * the navigation ref is ready.
75
+ */
76
+ export function loadRouteNode() {
77
+ const store = getExpoRouterStore();
78
+ if (!store) {
79
+ return null;
80
+ }
81
+ if (store.routeNode) {
82
+ hasLoggedMissingRouteNode = false;
83
+ lastRouteNodeTimestamp = Date.now();
84
+ return store.routeNode;
85
+ }
86
+ const isReady = store.navigationRef?.isReady?.();
87
+ if (__DEV__ && !hasLoggedMissingRouteNode && isReady) {
88
+ logOnce("Expo Router route tree is unavailable. Ensure your app directory is configured and that expo-router is initialized before opening the Route Events devtool.");
89
+ hasLoggedMissingRouteNode = true;
90
+ }
91
+ return null;
92
+ }
93
+ export function getRouteNodeMetadata() {
94
+ return {
95
+ source: cachedStoreSource,
96
+ lastLoadedAt: lastRouteNodeTimestamp
97
+ };
98
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+
3
+ // Export route events modal (primary export - everything you need!)
4
+ export { RouteEventsModalWithTabs } from "./components/RouteEventsModalWithTabs";
5
+ // Export preset configuration (easiest way to add to FloatingDevTools!)
6
+ export { routeEventsToolPreset, createRouteEventsTool } from "./preset";
7
+
8
+ // Export RouteTracker component - place inside your navigation tree for route tracking
9
+ export { RouteTracker } from "./RouteTracker";
10
+
11
+ // Export individual components for advanced usage
12
+ export { RoutesSitemap } from "./components/RoutesSitemap";
13
+ export { NavigationStack } from "./components/NavigationStack";
14
+ // Export advanced/optional utilities
15
+ // Note: Most users won't need these - the modal handles everything automatically
16
+ export { RouteObserver, routeObserver } from "./RouteObserver";
17
+ export { useRouteObserver } from "./useRouteObserver";
18
+ // Export route parser utilities for advanced use cases
19
+ export { RouteParser } from "./RouteParser";
20
+ // Export route sitemap hooks for advanced use cases
21
+ export { useRouteSitemap, useRoute, useParentRoutes } from "./useRouteSitemap";
22
+ // Export navigation stack utilities for advanced use cases
23
+ export { useNavigationStack } from "./useNavigationStack";
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Pre-configured route events tool for FloatingDevTools
5
+ *
6
+ * This preset provides a zero-config way to add route tracking to your dev tools.
7
+ * Just import and spread it into your apps array!
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * import { routeEventsToolPreset } from '@buoy-gg/route-events';
12
+ *
13
+ * const installedApps = [
14
+ * routeEventsToolPreset, // That's it!
15
+ * // ...other tools
16
+ * ];
17
+ * ```
18
+ */
19
+
20
+ import { RoutesIcon } from "@buoy-gg/floating-tools-core";
21
+ import { RouteEventsModalWithTabs } from "./components/RouteEventsModalWithTabs";
22
+
23
+ /**
24
+ * Pre-configured route events tool for FloatingDevTools.
25
+ * Includes:
26
+ * - Route sitemap browser
27
+ * - Event timeline with filtering
28
+ * - Navigation stack visualization
29
+ * - Automatic route tracking (no setup needed)
30
+ */
31
+ import { jsx as _jsx } from "react/jsx-runtime";
32
+ export const routeEventsToolPreset = {
33
+ id: "route-events",
34
+ name: "ROUTES",
35
+ description: "Route tracking & navigation inspector",
36
+ slot: "both",
37
+ icon: ({
38
+ size
39
+ }) => /*#__PURE__*/_jsx(RoutesIcon, {
40
+ size: size
41
+ }),
42
+ component: RouteEventsModalWithTabs,
43
+ props: {
44
+ enableSharedModalDimensions: false
45
+ }
46
+ };
47
+
48
+ /**
49
+ * Create a custom route events tool configuration.
50
+ * Use this if you want to override default settings.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * import { createRouteEventsTool } from '@buoy-gg/route-events';
55
+ *
56
+ * const myRouteTool = createRouteEventsTool({
57
+ * name: "MY ROUTES",
58
+ * color: "#a78bfa",
59
+ * enableSharedModalDimensions: true,
60
+ * });
61
+ * ```
62
+ */
63
+ export function createRouteEventsTool(options) {
64
+ return {
65
+ id: options?.id || "route-events",
66
+ name: options?.name || "ROUTES",
67
+ description: options?.description || "Route tracking & navigation inspector",
68
+ slot: "both",
69
+ icon: ({
70
+ size
71
+ }) => /*#__PURE__*/_jsx(RoutesIcon, {
72
+ size: size
73
+ }),
74
+ component: RouteEventsModalWithTabs,
75
+ props: {
76
+ enableSharedModalDimensions: options?.enableSharedModalDimensions !== undefined ? options.enableSharedModalDimensions : false
77
+ }
78
+ };
79
+ }
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * useNavigationStack - Hook to access current navigation stack state
5
+ *
6
+ * Provides real-time access to the navigation stack from Expo Router,
7
+ * showing what screens are currently mounted in memory and which is visible.
8
+ *
9
+ * Data source: @react-navigation/native
10
+ */
11
+
12
+ import { useState, useEffect, useMemo } from "react";
13
+ import { useNavigation, useNavigationState } from "@react-navigation/native";
14
+ import { router } from "expo-router";
15
+
16
+ // ============================================================================
17
+ // Type Definitions
18
+ // ============================================================================
19
+
20
+ // ============================================================================
21
+ // Main Hook
22
+ // ============================================================================
23
+
24
+ /**
25
+ * Recursively collect all routes from the navigation state tree
26
+ */
27
+ function collectRoutesFromState(state, depth = 0, parentPath = "") {
28
+ if (!state || !state.routes) return [];
29
+ const routes = [];
30
+ for (let i = 0; i < state.routes.length; i++) {
31
+ const route = state.routes[i];
32
+ const isFocused = i === state.index;
33
+
34
+ // Build the path
35
+ let pathname = route.name;
36
+ if (route.name === "index") {
37
+ pathname = "/";
38
+ } else if (!pathname.startsWith("/")) {
39
+ pathname = `/${pathname}`;
40
+ }
41
+
42
+ // Add parent path
43
+ if (parentPath && parentPath !== "/") {
44
+ pathname = `${parentPath}${pathname}`;
45
+ }
46
+
47
+ // Add params to pathname for dynamic routes
48
+ if (route.params) {
49
+ const paramEntries = Object.entries(route.params);
50
+ if (paramEntries.length > 0) {
51
+ pathname = pathname.replace(/\[([^\]]+)\]/g, (match, param) => {
52
+ return route.params[param] || match;
53
+ });
54
+ }
55
+ }
56
+ routes.push({
57
+ key: route.key,
58
+ name: route.name,
59
+ path: pathname,
60
+ params: route.params || {},
61
+ state: route.state
62
+ });
63
+
64
+ // If this route has nested state and is focused, recurse
65
+ if (isFocused && route.state) {
66
+ const nestedRoutes = collectRoutesFromState(route.state, depth + 1, pathname);
67
+ routes.push(...nestedRoutes);
68
+ }
69
+ }
70
+ return routes;
71
+ }
72
+
73
+ /**
74
+ * Access the current navigation stack from Expo Router
75
+ *
76
+ * @example
77
+ * ```tsx
78
+ * const { stack, focusedRoute, goBack, popToTop } = useNavigationStack();
79
+ *
80
+ * // Display stack
81
+ * stack.map(item => (
82
+ * <View key={item.key}>
83
+ * <Text>{item.pathname}</Text>
84
+ * {item.isFocused && <Text>VISIBLE</Text>}
85
+ * </View>
86
+ * ));
87
+ * ```
88
+ */
89
+ export function useNavigationStack() {
90
+ const [isLoaded, setIsLoaded] = useState(false);
91
+ const [error, setError] = useState(null);
92
+ const navigation = useNavigation();
93
+
94
+ // Subscribe to navigation state changes - this will cause re-renders when state updates
95
+ const navigationState = useNavigationState(state => state);
96
+
97
+ // Mark as loaded once we have navigation
98
+ useEffect(() => {
99
+ setIsLoaded(true);
100
+ }, []);
101
+
102
+ // Transform navigation state into display items
103
+ const stack = useMemo(() => {
104
+ if (!navigationState) return [];
105
+ try {
106
+ const routes = collectRoutesFromState(navigationState);
107
+
108
+ // Filter out internal Expo Router routes (layouts, __root, etc.)
109
+ const filteredRoutes = routes.filter(route => {
110
+ // Remove __root and other internal routes
111
+ if (route.name.startsWith("__")) return false;
112
+ if (route.name.includes("_layout")) return false;
113
+ if (route.name.startsWith("+not-found")) return false;
114
+ return true;
115
+ });
116
+
117
+ // If we filtered everything out, just keep the last route
118
+ const finalRoutes = filteredRoutes.length > 0 ? filteredRoutes : routes.slice(-1);
119
+
120
+ // The last route in the collected list is the focused one
121
+ return finalRoutes.map((route, index) => {
122
+ // Clean up pathname by removing /__root prefix
123
+ let cleanPath = route.path || `/${route.name}`;
124
+ cleanPath = cleanPath.replace(/^\/__root/, "");
125
+ // Ensure we always have at least a /
126
+ if (!cleanPath || cleanPath === "") {
127
+ cleanPath = "/";
128
+ }
129
+ return {
130
+ key: route.key,
131
+ name: route.name,
132
+ pathname: cleanPath,
133
+ params: route.params || {},
134
+ isFocused: index === finalRoutes.length - 1,
135
+ index,
136
+ canPop: index > 0
137
+ };
138
+ });
139
+ } catch (err) {
140
+ console.error("Error transforming navigation state:", err);
141
+ return [];
142
+ }
143
+ }, [navigationState]);
144
+
145
+ // Get focused route
146
+ const focusedRoute = useMemo(() => {
147
+ return stack.find(item => item.isFocused) || null;
148
+ }, [stack]);
149
+
150
+ // Helper properties
151
+ const stackDepth = stack.length;
152
+ const isAtRoot = stackDepth <= 1;
153
+
154
+ // Manual refresh (no-op since we're using React Navigation's state)
155
+ const refresh = () => {
156
+ // Navigation state updates automatically via useNavigationState
157
+ };
158
+
159
+ // Navigation actions
160
+ const navigateToIndex = index => {
161
+ if (index >= stack.length || index < 0) {
162
+ return;
163
+ }
164
+ const targetRoute = stack[index];
165
+ if (!targetRoute) {
166
+ return;
167
+ }
168
+
169
+ // Use router directly from expo-router
170
+ if (!router) {
171
+ return;
172
+ }
173
+ try {
174
+ router.navigate(targetRoute.pathname);
175
+ } catch (err) {
176
+ console.error("Failed to navigate:", err);
177
+ }
178
+ };
179
+ const popToIndex = index => {
180
+ if (index >= stack.length || index < 0) {
181
+ return;
182
+ }
183
+ const currentIndex = stack.length - 1;
184
+ const popCount = currentIndex - index;
185
+ if (popCount <= 0) {
186
+ return;
187
+ }
188
+
189
+ // Use router directly from expo-router
190
+ if (!router) {
191
+ return;
192
+ }
193
+ try {
194
+ // Pop multiple times
195
+ for (let i = 0; i < popCount; i++) {
196
+ router.back();
197
+ }
198
+ } catch (err) {
199
+ console.error("Failed to pop:", err);
200
+ }
201
+ };
202
+ const goBack = () => {
203
+ if (isAtRoot) {
204
+ return;
205
+ }
206
+
207
+ // Use router directly from expo-router
208
+ if (!router) {
209
+ return;
210
+ }
211
+ try {
212
+ router.back();
213
+ } catch (err) {
214
+ console.error("Failed to go back:", err);
215
+ }
216
+ };
217
+ const popToTop = () => {
218
+ if (isAtRoot) {
219
+ return;
220
+ }
221
+
222
+ // Pop to index 0 (root)
223
+ popToIndex(0);
224
+ };
225
+ return {
226
+ stack,
227
+ focusedRoute,
228
+ stackDepth,
229
+ isAtRoot,
230
+ isLoaded,
231
+ error,
232
+ refresh,
233
+ navigateToIndex,
234
+ popToIndex,
235
+ goBack,
236
+ popToTop
237
+ };
238
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * useRouteObserver - React hook for observing route changes
5
+ *
6
+ * Uses public Expo Router hooks to track route changes and emits them
7
+ * to the global RouteObserver singleton.
8
+ */
9
+
10
+ import { useEffect, useRef } from "react";
11
+ import { usePathname, useSegments, useGlobalSearchParams } from "expo-router";
12
+ import { routeObserver } from "./RouteObserver";
13
+
14
+ /**
15
+ * Hook to observe route changes in Expo Router
16
+ * Automatically emits events to the global RouteObserver
17
+ *
18
+ * @param callback - Optional function to call on route changes (in addition to the observer)
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * // Just track routes (no custom callback)
23
+ * useRouteObserver();
24
+ *
25
+ * // Track routes with custom callback
26
+ * useRouteObserver((event) => {
27
+ * // Handle route change
28
+ * analytics.trackPageView(event.pathname);
29
+ * });
30
+ * ```
31
+ */
32
+ export function useRouteObserver(callback) {
33
+ const pathname = usePathname();
34
+ const segments = useSegments();
35
+ const params = useGlobalSearchParams();
36
+ const callbackRef = useRef(callback);
37
+ const previousPathnameRef = useRef(undefined);
38
+ const previousTimestampRef = useRef(undefined);
39
+
40
+ // Update ref when callback changes
41
+ useEffect(() => {
42
+ callbackRef.current = callback;
43
+ }, [callback]);
44
+
45
+ // Trigger observer and callback whenever route changes
46
+ useEffect(() => {
47
+ const now = Date.now();
48
+ const timeSincePrevious = previousTimestampRef.current ? now - previousTimestampRef.current : undefined;
49
+ const event = {
50
+ pathname,
51
+ params: params,
52
+ segments: segments,
53
+ timestamp: now,
54
+ previousPathname: previousPathnameRef.current,
55
+ timeSincePrevious
56
+ };
57
+
58
+ // Update refs for next navigation
59
+ previousPathnameRef.current = pathname;
60
+ previousTimestampRef.current = now;
61
+
62
+ // Emit to the global observer (this notifies the modal)
63
+ routeObserver.emit(event);
64
+
65
+ // Also call the custom callback if provided
66
+ if (callbackRef.current) {
67
+ callbackRef.current(event);
68
+ }
69
+ }, [pathname, segments, params]);
70
+ }