@buoy-gg/route-events 7.0.41 → 7.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -64
- package/lib/commonjs/RouteObserver.js +1 -54
- package/lib/commonjs/RouteParser.js +1 -314
- package/lib/commonjs/RouteTracker.js +1 -112
- package/lib/commonjs/RouteTracker.web.js +1 -12
- package/lib/commonjs/browserRouteRegistry.js +1 -30
- package/lib/commonjs/browserRoutes.js +1 -96
- package/lib/commonjs/components/NavigationStack.js +2 -572
- package/lib/commonjs/components/ReactNavigationRoutes.js +1 -836
- package/lib/commonjs/components/ReactNavigationRoutes.web.js +1 -12
- package/lib/commonjs/components/RouteEventDetail.js +1 -97
- package/lib/commonjs/components/RouteEventExpandedContent.js +1 -204
- package/lib/commonjs/components/RouteEventItemCompact.js +1 -191
- package/lib/commonjs/components/RouteEventsModalWithTabs.js +1 -734
- package/lib/commonjs/components/RouteEventsTimeline.js +1 -107
- package/lib/commonjs/components/RouteFilterViewV2.js +1 -42
- package/lib/commonjs/components/RoutesSitemap.js +2 -1035
- package/lib/commonjs/expoRouterStore.js +1 -121
- package/lib/commonjs/expoRouterStore.web.js +1 -44
- package/lib/commonjs/hooks/useRouteEvents.js +1 -57
- package/lib/commonjs/index.js +1 -168
- package/lib/commonjs/preset.js +1 -69
- package/lib/commonjs/stores/navigationStackStore.js +1 -45
- package/lib/commonjs/stores/routeEventStore.js +1 -136
- package/lib/commonjs/sync/routeEventsSyncAdapter.js +1 -275
- package/lib/commonjs/useNavigationStack.js +1 -408
- package/lib/commonjs/useNavigationStack.web.js +1 -32
- package/lib/commonjs/useRouteObserver.js +1 -73
- package/lib/commonjs/useRouteObserverReactNavigation.js +1 -109
- package/lib/commonjs/useRouteSitemap.js +1 -255
- package/lib/commonjs/utils/routeTemplate.js +1 -79
- package/lib/commonjs/utils/safeReactNavigation.js +1 -194
- package/lib/commonjs/utils/safeReactNavigation.web.js +1 -36
- package/lib/commonjs/utils/testIDs.js +1 -63
- package/lib/module/RouteObserver.js +1 -49
- package/lib/module/RouteParser.js +1 -309
- package/lib/module/RouteTracker.js +1 -109
- package/lib/module/RouteTracker.web.js +1 -8
- package/lib/module/browserRouteRegistry.js +1 -24
- package/lib/module/browserRoutes.js +1 -92
- package/lib/module/components/NavigationStack.js +2 -568
- package/lib/module/components/ReactNavigationRoutes.js +1 -832
- package/lib/module/components/ReactNavigationRoutes.web.js +1 -3
- package/lib/module/components/RouteEventDetail.js +1 -93
- package/lib/module/components/RouteEventExpandedContent.js +1 -200
- package/lib/module/components/RouteEventItemCompact.js +1 -187
- package/lib/module/components/RouteEventsModalWithTabs.js +1 -731
- package/lib/module/components/RouteEventsTimeline.js +1 -103
- package/lib/module/components/RouteFilterViewV2.js +1 -38
- package/lib/module/components/RoutesSitemap.js +2 -1033
- package/lib/module/expoRouterStore.js +1 -114
- package/lib/module/expoRouterStore.web.js +1 -37
- package/lib/module/hooks/useRouteEvents.js +1 -54
- package/lib/module/index.js +1 -70
- package/lib/module/preset.js +1 -66
- package/lib/module/stores/navigationStackStore.js +1 -41
- package/lib/module/stores/routeEventStore.js +1 -128
- package/lib/module/sync/routeEventsSyncAdapter.js +1 -273
- package/lib/module/useNavigationStack.js +1 -404
- package/lib/module/useNavigationStack.web.js +1 -28
- package/lib/module/useRouteObserver.js +1 -70
- package/lib/module/useRouteObserverReactNavigation.js +1 -106
- package/lib/module/useRouteSitemap.js +1 -250
- package/lib/module/utils/routeTemplate.js +1 -73
- package/lib/module/utils/safeReactNavigation.js +1 -185
- package/lib/module/utils/safeReactNavigation.web.js +1 -27
- package/lib/module/utils/testIDs.js +1 -55
- package/lib/web/index.mjs +4 -5
- package/package.json +6 -11
- package/lib/typescript/RouteObserver.d.ts.map +0 -1
- package/lib/typescript/RouteParser.d.ts.map +0 -1
- package/lib/typescript/RouteTracker.d.ts.map +0 -1
- package/lib/typescript/RouteTracker.web.d.ts.map +0 -1
- package/lib/typescript/browserRouteRegistry.d.ts.map +0 -1
- package/lib/typescript/browserRoutes.d.ts.map +0 -1
- package/lib/typescript/components/NavigationStack.d.ts.map +0 -1
- package/lib/typescript/components/ReactNavigationRoutes.d.ts.map +0 -1
- package/lib/typescript/components/ReactNavigationRoutes.web.d.ts.map +0 -1
- package/lib/typescript/components/RouteEventDetail.d.ts.map +0 -1
- package/lib/typescript/components/RouteEventExpandedContent.d.ts.map +0 -1
- package/lib/typescript/components/RouteEventItemCompact.d.ts.map +0 -1
- package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +0 -1
- package/lib/typescript/components/RouteEventsTimeline.d.ts.map +0 -1
- package/lib/typescript/components/RouteFilterViewV2.d.ts.map +0 -1
- package/lib/typescript/components/RoutesSitemap.d.ts.map +0 -1
- package/lib/typescript/expoRouterStore.d.ts.map +0 -1
- package/lib/typescript/expoRouterStore.web.d.ts.map +0 -1
- package/lib/typescript/hooks/useRouteEvents.d.ts.map +0 -1
- package/lib/typescript/index.d.ts.map +0 -1
- package/lib/typescript/preset.d.ts.map +0 -1
- package/lib/typescript/stores/navigationStackStore.d.ts.map +0 -1
- package/lib/typescript/stores/routeEventStore.d.ts.map +0 -1
- package/lib/typescript/sync/routeEventsSyncAdapter.d.ts.map +0 -1
- package/lib/typescript/useNavigationStack.d.ts.map +0 -1
- package/lib/typescript/useNavigationStack.web.d.ts.map +0 -1
- package/lib/typescript/useRouteObserver.d.ts.map +0 -1
- package/lib/typescript/useRouteObserverReactNavigation.d.ts.map +0 -1
- package/lib/typescript/useRouteSitemap.d.ts.map +0 -1
- package/lib/typescript/utils/routeTemplate.d.ts.map +0 -1
- package/lib/typescript/utils/safeReactNavigation.d.ts.map +0 -1
- package/lib/typescript/utils/safeReactNavigation.web.d.ts.map +0 -1
- package/lib/typescript/utils/testIDs.d.ts.map +0 -1
|
@@ -1,28 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { useSyncExternalStore } from 'react';
|
|
4
|
-
import { navigationStackStore } from './stores/navigationStackStore';
|
|
5
|
-
const empty = [];
|
|
6
|
-
export function useNavigationStack() {
|
|
7
|
-
const stack = useSyncExternalStore(callback => navigationStackStore.subscribe(callback), () => navigationStackStore.getStack(), () => empty);
|
|
8
|
-
const focused = stack.find(item => item.isFocused) ?? null;
|
|
9
|
-
const actions = () => {
|
|
10
|
-
const current = navigationStackStore.getActions();
|
|
11
|
-
if (!current) throw new Error('Mount RouteTracker to capture browser navigation.');
|
|
12
|
-
return current;
|
|
13
|
-
};
|
|
14
|
-
return {
|
|
15
|
-
stack,
|
|
16
|
-
focusedRoute: focused,
|
|
17
|
-
stackDepth: stack.length,
|
|
18
|
-
isAtRoot: !focused || focused.index === 0,
|
|
19
|
-
isLoaded: stack.length > 0,
|
|
20
|
-
error: null,
|
|
21
|
-
refresh: () => {},
|
|
22
|
-
navigateToIndex: index => actions().navigateToIndex(index),
|
|
23
|
-
navigateToRoute: (name, params) => actions().navigateToRoute?.(name, params) ?? false,
|
|
24
|
-
popToIndex: index => actions().popToIndex(index),
|
|
25
|
-
goBack: () => actions().goBack(),
|
|
26
|
-
popToTop: () => actions().popToTop()
|
|
27
|
-
};
|
|
28
|
-
}
|
|
1
|
+
"use strict";import{useSyncExternalStore as c}from"react";import{navigationStackStore as r}from"./stores/navigationStackStore";const s=[];export function useNavigationStack(){const e=c(o=>r.subscribe(o),()=>r.getStack(),()=>s);const n=e.find(o=>o.isFocused)??null;const t=()=>{const o=r.getActions();if(!o)throw new Error("Mount RouteTracker to capture browser navigation.");return o};return{stack:e,focusedRoute:n,stackDepth:e.length,isAtRoot:!n||n.index===0,isLoaded:e.length>0,error:null,refresh:()=>{},navigateToIndex:o=>t().navigateToIndex(o),navigateToRoute:(o,a)=>t().navigateToRoute?.(o,a)??false,popToIndex:o=>t().popToIndex(o),goBack:()=>t().goBack(),popToTop:()=>t().popToTop()}}
|
|
@@ -1,70 +1 @@
|
|
|
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 { useSafePathname, useSafeSegments, useSafeGlobalSearchParams } from "@buoy-gg/shared-ui";
|
|
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 = useSafePathname();
|
|
34
|
-
const segments = useSafeSegments();
|
|
35
|
-
const params = useSafeGlobalSearchParams();
|
|
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
|
-
}
|
|
1
|
+
"use strict";import{useEffect as i,useRef as o}from"react";import{useSafePathname as p,useSafeSegments as v,useSafeGlobalSearchParams as d}from"@buoy-gg/shared-ui";import{routeObserver as S}from"./RouteObserver";export function useRouteObserver(e){const t=p();const u=v();const m=d();const r=o(e);const a=o(void 0);const n=o(void 0);i(()=>{r.current=e},[e]);i(()=>{const s=Date.now();const f=n.current?s-n.current:void 0;const c={pathname:t,params:m,segments:u,timestamp:s,previousPathname:a.current,timeSincePrevious:f};a.current=t;n.current=s;S.emit(c);if(r.current){r.current(c)}},[t,u,m])}
|
|
@@ -1,106 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* useRouteObserverReactNavigation - React hook for observing route changes
|
|
5
|
-
* using @react-navigation/native APIs.
|
|
6
|
-
*
|
|
7
|
-
* This is the React Navigation (RN CLI) counterpart to useRouteObserver.ts
|
|
8
|
-
* which uses Expo Router hooks. Both emit to the same routeObserver singleton.
|
|
9
|
-
*
|
|
10
|
-
* Uses NavigationContainerRefContext so it works anywhere inside
|
|
11
|
-
* a NavigationContainer — does NOT need to be inside a navigator.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { useEffect, useRef } from "react";
|
|
15
|
-
import { useSafeNavigationContainerRef } from "./utils/safeReactNavigation";
|
|
16
|
-
import { routeObserver } from "./RouteObserver";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Walk a React Navigation state tree to find the deepest focused route.
|
|
20
|
-
* Returns the route object and the array of route names from root to leaf.
|
|
21
|
-
*/
|
|
22
|
-
function getDeepestFocusedRoute(state) {
|
|
23
|
-
if (!state || !state.routes || state.routes.length === 0) {
|
|
24
|
-
return {
|
|
25
|
-
route: null,
|
|
26
|
-
segments: []
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
const route = state.routes[state.index ?? 0];
|
|
30
|
-
const segments = [route.name];
|
|
31
|
-
if (route.state) {
|
|
32
|
-
const nested = getDeepestFocusedRoute(route.state);
|
|
33
|
-
if (nested.route) {
|
|
34
|
-
return {
|
|
35
|
-
route: nested.route,
|
|
36
|
-
segments: [...segments, ...nested.segments]
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return {
|
|
41
|
-
route,
|
|
42
|
-
segments
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
function emitRouteEvent(state, previousPathnameRef, previousTimestampRef, callbackRef) {
|
|
46
|
-
if (!state) return;
|
|
47
|
-
const {
|
|
48
|
-
route,
|
|
49
|
-
segments
|
|
50
|
-
} = getDeepestFocusedRoute(state);
|
|
51
|
-
if (!route) return;
|
|
52
|
-
const pathname = `/${route.name}`;
|
|
53
|
-
|
|
54
|
-
// Skip if pathname hasn't changed
|
|
55
|
-
if (pathname === previousPathnameRef.current) return;
|
|
56
|
-
const now = Date.now();
|
|
57
|
-
const timeSincePrevious = previousTimestampRef.current ? now - previousTimestampRef.current : undefined;
|
|
58
|
-
const event = {
|
|
59
|
-
pathname,
|
|
60
|
-
params: route.params || {},
|
|
61
|
-
segments,
|
|
62
|
-
timestamp: now,
|
|
63
|
-
previousPathname: previousPathnameRef.current,
|
|
64
|
-
timeSincePrevious
|
|
65
|
-
};
|
|
66
|
-
previousPathnameRef.current = pathname;
|
|
67
|
-
previousTimestampRef.current = now;
|
|
68
|
-
routeObserver.emit(event);
|
|
69
|
-
if (callbackRef.current) {
|
|
70
|
-
callbackRef.current(event);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Hook to observe route changes via @react-navigation/native.
|
|
76
|
-
* Automatically emits events to the global RouteObserver.
|
|
77
|
-
*
|
|
78
|
-
* Works anywhere inside a NavigationContainer (does not need to be inside a navigator).
|
|
79
|
-
*/
|
|
80
|
-
export function useRouteObserverReactNavigation(callback) {
|
|
81
|
-
const containerRef = useSafeNavigationContainerRef();
|
|
82
|
-
const callbackRef = useRef(callback);
|
|
83
|
-
const previousPathnameRef = useRef(undefined);
|
|
84
|
-
const previousTimestampRef = useRef(undefined);
|
|
85
|
-
useEffect(() => {
|
|
86
|
-
callbackRef.current = callback;
|
|
87
|
-
}, [callback]);
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
if (!containerRef) return;
|
|
90
|
-
|
|
91
|
-
// Emit for the initial state
|
|
92
|
-
const initialState = containerRef.getRootState?.();
|
|
93
|
-
if (initialState) {
|
|
94
|
-
emitRouteEvent(initialState, previousPathnameRef, previousTimestampRef, callbackRef);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Listen for state changes
|
|
98
|
-
const unsubscribe = containerRef.addListener?.("state", () => {
|
|
99
|
-
const state = containerRef.getRootState?.();
|
|
100
|
-
emitRouteEvent(state, previousPathnameRef, previousTimestampRef, callbackRef);
|
|
101
|
-
});
|
|
102
|
-
return () => {
|
|
103
|
-
unsubscribe?.();
|
|
104
|
-
};
|
|
105
|
-
}, [containerRef]);
|
|
106
|
-
}
|
|
1
|
+
"use strict";import{useEffect as m,useRef as c}from"react";import{useSafeNavigationContainerRef as p}from"./utils/safeReactNavigation";import{routeObserver as R}from"./RouteObserver";function a(t){if(!t||!t.routes||t.routes.length===0){return{route:null,segments:[]}}const e=t.routes[t.index??0];const n=[e.name];if(e.state){const r=a(e.state);if(r.route){return{route:r.route,segments:[...n,...r.segments]}}}return{route:e,segments:n}}function d(t,e,n,r){if(!t)return;const{route:o,segments:i}=a(t);if(!o)return;const u=`/${o.name}`;if(u===e.current)return;const s=Date.now();const g=n.current?s-n.current:void 0;const f={pathname:u,params:o.params||{},segments:i,timestamp:s,previousPathname:e.current,timeSincePrevious:g};e.current=u;n.current=s;R.emit(f);if(r.current){r.current(f)}}export function useRouteObserverReactNavigation(t){const e=p();const n=c(t);const r=c(void 0);const o=c(void 0);m(()=>{n.current=t},[t]);m(()=>{if(!e)return;const i=e.getRootState?.();if(i){d(i,r,o,n)}const u=e.addListener?.("state",()=>{const s=e.getRootState?.();d(s,r,o,n)});return()=>{u?.()}},[e])}
|
|
@@ -1,250 +1 @@
|
|
|
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, isExpoRouterStoreSupported, 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
|
-
injectedRoutes,
|
|
96
|
-
injectedSource = null,
|
|
97
|
-
injectedLastUpdatedAt = null
|
|
98
|
-
} = options;
|
|
99
|
-
const hasInjectedRoutes = injectedRoutes != null;
|
|
100
|
-
const [routeTreeState, setRouteTreeState] = useState(() => {
|
|
101
|
-
const node = loadRouteNode();
|
|
102
|
-
const metadata = getRouteNodeMetadata();
|
|
103
|
-
return {
|
|
104
|
-
node,
|
|
105
|
-
version: 0,
|
|
106
|
-
lastUpdatedAt: metadata.lastLoadedAt,
|
|
107
|
-
source: metadata.source
|
|
108
|
-
};
|
|
109
|
-
});
|
|
110
|
-
const routeNode = routeTreeState.node;
|
|
111
|
-
const routeNodeVersion = routeTreeState.version;
|
|
112
|
-
const lastUpdatedAt = routeTreeState.lastUpdatedAt;
|
|
113
|
-
const source = routeTreeState.source;
|
|
114
|
-
const refresh = useCallback(() => {
|
|
115
|
-
setRouteTreeState(previous => {
|
|
116
|
-
const node = loadRouteNode();
|
|
117
|
-
const metadata = getRouteNodeMetadata();
|
|
118
|
-
return {
|
|
119
|
-
node,
|
|
120
|
-
version: previous.version + 1,
|
|
121
|
-
lastUpdatedAt: metadata.lastLoadedAt,
|
|
122
|
-
source: metadata.source
|
|
123
|
-
};
|
|
124
|
-
});
|
|
125
|
-
}, []);
|
|
126
|
-
|
|
127
|
-
// When the route tree isn't available yet (e.g., Expo Router still mounting),
|
|
128
|
-
// poll until it becomes ready. Use a more aggressive retry since the store
|
|
129
|
-
// is a singleton that gets populated asynchronously.
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
if (routeNode) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Routes injected from outside (dashboard) don't come from the local store.
|
|
136
|
-
if (hasInjectedRoutes) {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// On web (no expo-router) the tree will never load — don't poll forever.
|
|
141
|
-
if (!isExpoRouterStoreSupported()) {
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
let retryCount = 0;
|
|
145
|
-
const maxRetries = 100; // 10 seconds max
|
|
146
|
-
|
|
147
|
-
const poll = () => {
|
|
148
|
-
retryCount++;
|
|
149
|
-
refresh();
|
|
150
|
-
if (retryCount < maxRetries) {
|
|
151
|
-
timeoutRef = setTimeout(poll, 100);
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
let timeoutRef = setTimeout(poll, 100);
|
|
155
|
-
return () => clearTimeout(timeoutRef);
|
|
156
|
-
}, [routeNode, refresh, hasInjectedRoutes]);
|
|
157
|
-
|
|
158
|
-
// Optional auto-refresh hook for callers that want periodic updates
|
|
159
|
-
useEffect(() => {
|
|
160
|
-
if (!autoRefresh) return;
|
|
161
|
-
const interval = setInterval(refresh, refreshInterval);
|
|
162
|
-
return () => clearInterval(interval);
|
|
163
|
-
}, [autoRefresh, refreshInterval, refresh]);
|
|
164
|
-
|
|
165
|
-
// With injected routes we're "supported" regardless of the local runtime, and
|
|
166
|
-
// "loaded" once the device has actually sent a non-empty tree (an empty array
|
|
167
|
-
// means the device is still booting expo-router → keep showing "Loading...").
|
|
168
|
-
const isLoaded = hasInjectedRoutes ? injectedRoutes.length > 0 : !!routeNode;
|
|
169
|
-
const isSupported = hasInjectedRoutes ? true : isExpoRouterStoreSupported();
|
|
170
|
-
|
|
171
|
-
// Parse routes (or use the pre-parsed routes injected from the dashboard).
|
|
172
|
-
const routes = useMemo(() => {
|
|
173
|
-
if (hasInjectedRoutes) return injectedRoutes;
|
|
174
|
-
if (!routeNode) return [];
|
|
175
|
-
return RouteParser.parseRouteTree(routeNode);
|
|
176
|
-
}, [routeNode, routeNodeVersion, hasInjectedRoutes, injectedRoutes]);
|
|
177
|
-
|
|
178
|
-
// Sort routes
|
|
179
|
-
const sortedRoutes = useMemo(() => {
|
|
180
|
-
return RouteParser.sortRoutes(routes, sortBy);
|
|
181
|
-
}, [routes, sortBy]);
|
|
182
|
-
|
|
183
|
-
// Filter routes by search query
|
|
184
|
-
const filteredRoutes = useMemo(() => {
|
|
185
|
-
if (!searchQuery) return sortedRoutes;
|
|
186
|
-
return RouteParser.filterRoutes(sortedRoutes, searchQuery);
|
|
187
|
-
}, [sortedRoutes, searchQuery]);
|
|
188
|
-
|
|
189
|
-
// Organize into groups
|
|
190
|
-
const groups = useMemo(() => {
|
|
191
|
-
return RouteParser.organizeRoutes(filteredRoutes);
|
|
192
|
-
}, [filteredRoutes]);
|
|
193
|
-
|
|
194
|
-
// Calculate stats
|
|
195
|
-
const stats = useMemo(() => {
|
|
196
|
-
return RouteParser.getRouteStats(routes);
|
|
197
|
-
}, [routes]);
|
|
198
|
-
|
|
199
|
-
// Helper functions
|
|
200
|
-
const findRoute = path => RouteParser.findRouteByPath(routes, path);
|
|
201
|
-
const getParents = path => RouteParser.getParentRoutes(routes, path);
|
|
202
|
-
return {
|
|
203
|
-
routes: sortedRoutes,
|
|
204
|
-
groups,
|
|
205
|
-
stats,
|
|
206
|
-
filteredRoutes,
|
|
207
|
-
isLoaded,
|
|
208
|
-
isSupported,
|
|
209
|
-
refresh,
|
|
210
|
-
findRoute,
|
|
211
|
-
getParents,
|
|
212
|
-
lastUpdatedAt: hasInjectedRoutes ? injectedLastUpdatedAt : lastUpdatedAt,
|
|
213
|
-
source: hasInjectedRoutes ? injectedSource : source
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// ============================================================================
|
|
218
|
-
// Helper Hooks
|
|
219
|
-
// ============================================================================
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Get a specific route by path
|
|
223
|
-
*
|
|
224
|
-
* @example
|
|
225
|
-
* ```tsx
|
|
226
|
-
* const pokemonRoute = useRoute('/pokemon/[id]');
|
|
227
|
-
* ```
|
|
228
|
-
*/
|
|
229
|
-
export function useRoute(path) {
|
|
230
|
-
const {
|
|
231
|
-
findRoute
|
|
232
|
-
} = useRouteSitemap();
|
|
233
|
-
return useMemo(() => findRoute(path), [findRoute, path]);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Get all parent routes for a given path
|
|
238
|
-
*
|
|
239
|
-
* @example
|
|
240
|
-
* ```tsx
|
|
241
|
-
* const parents = useParentRoutes('/pokemon/[id]');
|
|
242
|
-
* // Returns: [{ path: '/pokemon', ... }]
|
|
243
|
-
* ```
|
|
244
|
-
*/
|
|
245
|
-
export function useParentRoutes(path) {
|
|
246
|
-
const {
|
|
247
|
-
getParents
|
|
248
|
-
} = useRouteSitemap();
|
|
249
|
-
return useMemo(() => getParents(path), [getParents, path]);
|
|
250
|
-
}
|
|
1
|
+
"use strict";import{useState as C,useEffect as N,useMemo as u,useCallback as K}from"react";import{RouteParser as s}from"./RouteParser";import{getRouteNodeMetadata as S,isExpoRouterStoreSupported as v,loadRouteNode as P}from"./expoRouterStore";function z(e){if(!e||!e.routes)return null;const r=e.routes?.[0];if(!r)return null;const c={type:"route",route:"",dynamic:null,children:[],contextKey:"_app"};function p(a,i,g=""){if(!a||!a.routes)return;a.routes.forEach(R=>{const t=R.name;if(t.startsWith("__")||t.includes("_layout")||t.startsWith("+not-found")){return}const n={type:t==="index"?"route":"route",route:t==="index"?"":t,dynamic:t.includes("[")?[t.match(/\[([^\]]+)\]/)?.[1]||""]:null,children:[],contextKey:`app/${t}`};i.children.push(n);if(R.state){p(R.state,n,`${g}/${t}`)}})}p(e,c);return c.children.length>0?c:null}export function useRouteSitemap(e={}){const{searchQuery:r="",sortBy:c="path",autoRefresh:p=false,refreshInterval:a=1e3,injectedRoutes:i,injectedSource:g=null,injectedLastUpdatedAt:R=null}=e;const t=i!=null;const[n,T]=C(()=>{const o=P();const f=S();return{node:o,version:0,lastUpdatedAt:f.lastLoadedAt,source:f.source}});const d=n.node;const U=n.version;const j=n.lastUpdatedAt;const I=n.source;const m=K(()=>{T(o=>{const f=P();const h=S();return{node:f,version:o.version+1,lastUpdatedAt:h.lastLoadedAt,source:h.source}})},[]);N(()=>{if(d){return}if(t){return}if(!v()){return}let o=0;const f=100;const h=()=>{o++;m();if(o<f){A=setTimeout(h,100)}};let A=setTimeout(h,100);return()=>clearTimeout(A)},[d,m,t]);N(()=>{if(!p)return;const o=setInterval(m,a);return()=>clearInterval(o)},[p,a,m]);const L=t?i.length>0:!!d;const _=t?true:v();const l=u(()=>{if(t)return i;if(!d)return[];return s.parseRouteTree(d)},[d,U,t,i]);const y=u(()=>{return s.sortRoutes(l,c)},[l,c]);const x=u(()=>{if(!r)return y;return s.filterRoutes(y,r)},[y,r]);const E=u(()=>{return s.organizeRoutes(x)},[x]);const $=u(()=>{return s.getRouteStats(l)},[l]);const b=o=>s.findRouteByPath(l,o);const B=o=>s.getParentRoutes(l,o);return{routes:y,groups:E,stats:$,filteredRoutes:x,isLoaded:L,isSupported:_,refresh:m,findRoute:b,getParents:B,lastUpdatedAt:t?R:j,source:t?g:I}}export function useRoute(e){const{findRoute:r}=useRouteSitemap();return u(()=>r(e),[r,e])}export function useParentRoutes(e){const{getParents:r}=useRouteSitemap();return u(()=>r(e),[r,e])}
|
|
@@ -1,73 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Infer a route template from a concrete pathname and its segments.
|
|
5
|
-
* Dynamic segments (numeric ids, UUIDs) collapse to `[id]`, so
|
|
6
|
-
* `/products/42` becomes `/products/[id]`. Returns null when the template
|
|
7
|
-
* matches the pathname (i.e. the route is fully static).
|
|
8
|
-
*/
|
|
9
|
-
export function getRouteTemplate(pathname, segments) {
|
|
10
|
-
if (!segments || segments.length === 0) return null;
|
|
11
|
-
const templateParts = segments.map(segment => {
|
|
12
|
-
// Numeric ids
|
|
13
|
-
if (/^\d+$/.test(segment)) {
|
|
14
|
-
return "[id]";
|
|
15
|
-
}
|
|
16
|
-
// UUIDs
|
|
17
|
-
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {
|
|
18
|
-
return "[id]";
|
|
19
|
-
}
|
|
20
|
-
return segment;
|
|
21
|
-
});
|
|
22
|
-
const template = "/" + templateParts.join("/");
|
|
23
|
-
return template !== pathname ? template : null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* The href that reopens a recorded route: its pathname plus the params that are
|
|
28
|
-
* not already in the path, as a query string. Expo Router reports dynamic
|
|
29
|
-
* segment values (`/order/42` → `{ id: "42" }`) and search params (`?menu=1`)
|
|
30
|
-
* in one `params` object; only the second kind needs to be carried, because the
|
|
31
|
-
* first is already in the pathname.
|
|
32
|
-
*/
|
|
33
|
-
export function routeHref(pathname, params) {
|
|
34
|
-
if (!params) return pathname;
|
|
35
|
-
const inPath = new Set(pathname.split("/").filter(Boolean).map(seg => {
|
|
36
|
-
try {
|
|
37
|
-
return decodeURIComponent(seg);
|
|
38
|
-
} catch {
|
|
39
|
-
return seg;
|
|
40
|
-
}
|
|
41
|
-
}));
|
|
42
|
-
const query = [];
|
|
43
|
-
for (const [key, raw] of Object.entries(params)) {
|
|
44
|
-
const values = Array.isArray(raw) ? raw : [raw];
|
|
45
|
-
// A catch-all segment's values, or one dynamic segment's value.
|
|
46
|
-
if (values.length > 0 && values.every(v => inPath.has(String(v)))) continue;
|
|
47
|
-
for (const v of values) query.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`);
|
|
48
|
-
}
|
|
49
|
-
return query.length ? `${pathname}?${query.join("&")}` : pathname;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Split an href from {@link routeHref} back into its path and params. */
|
|
53
|
-
export function splitRouteHref(href) {
|
|
54
|
-
const q = href.indexOf("?");
|
|
55
|
-
if (q < 0) return {
|
|
56
|
-
pathname: href,
|
|
57
|
-
params: {}
|
|
58
|
-
};
|
|
59
|
-
const params = {};
|
|
60
|
-
for (const pair of href.slice(q + 1).split("&")) {
|
|
61
|
-
if (!pair) continue;
|
|
62
|
-
const [k, v = ""] = pair.split("=");
|
|
63
|
-
try {
|
|
64
|
-
params[decodeURIComponent(k)] = decodeURIComponent(v);
|
|
65
|
-
} catch {
|
|
66
|
-
params[k] = v;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return {
|
|
70
|
-
pathname: href.slice(0, q),
|
|
71
|
-
params
|
|
72
|
-
};
|
|
73
|
-
}
|
|
1
|
+
"use strict";export function getRouteTemplate(t,e){if(!e||e.length===0)return null;const r=e.map(n=>{if(/^\d+$/.test(n)){return"[id]"}if(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(n)){return"[id]"}return n});const o="/"+r.join("/");return o!==t?o:null}export function routeHref(t,e){if(!e)return t;const r=new Set(t.split("/").filter(Boolean).map(n=>{try{return decodeURIComponent(n)}catch{return n}}));const o=[];for(const[n,i]of Object.entries(e)){const c=Array.isArray(i)?i:[i];if(c.length>0&&c.every(u=>r.has(String(u))))continue;for(const u of c)o.push(`${encodeURIComponent(n)}=${encodeURIComponent(String(u))}`)}return o.length?`${t}?${o.join("&")}`:t}export function splitRouteHref(t){const e=t.indexOf("?");if(e<0)return{pathname:t,params:{}};const r={};for(const o of t.slice(e+1).split("&")){if(!o)continue;const[n,i=""]=o.split("=");try{r[decodeURIComponent(n)]=decodeURIComponent(i)}catch{r[n]=i}}return{pathname:t.slice(0,e),params:r}}
|