@buoy-gg/route-events 7.0.40 → 7.0.41
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 +4 -0
- package/lib/commonjs/RouteTracker.web.js +12 -0
- package/lib/commonjs/browserRouteRegistry.js +30 -0
- package/lib/commonjs/browserRoutes.js +96 -0
- package/lib/commonjs/components/ReactNavigationRoutes.web.js +12 -0
- package/lib/commonjs/components/RouteEventDetail.js +1 -1
- package/lib/commonjs/components/RouteEventItemCompact.js +1 -1
- package/lib/commonjs/components/RouteEventsModalWithTabs.js +24 -7
- package/lib/commonjs/expoRouterStore.web.js +44 -0
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/useNavigationStack.web.js +32 -0
- package/lib/commonjs/utils/routeTemplate.js +51 -0
- package/lib/commonjs/utils/safeReactNavigation.web.js +36 -0
- package/lib/module/RouteTracker.web.js +8 -0
- package/lib/module/browserRouteRegistry.js +24 -0
- package/lib/module/browserRoutes.js +92 -0
- package/lib/module/components/ReactNavigationRoutes.web.js +3 -0
- package/lib/module/components/RouteEventDetail.js +2 -2
- package/lib/module/components/RouteEventItemCompact.js +2 -2
- package/lib/module/components/RouteEventsModalWithTabs.js +24 -7
- package/lib/module/expoRouterStore.web.js +37 -0
- package/lib/module/index.js +1 -0
- package/lib/module/useNavigationStack.web.js +28 -0
- package/lib/module/utils/routeTemplate.js +49 -0
- package/lib/module/utils/safeReactNavigation.web.js +27 -0
- package/lib/typescript/RouteTracker.web.d.ts +2 -0
- package/lib/typescript/RouteTracker.web.d.ts.map +1 -0
- package/lib/typescript/browserRouteRegistry.d.ts +4 -0
- package/lib/typescript/browserRouteRegistry.d.ts.map +1 -0
- package/lib/typescript/browserRoutes.d.ts +2 -0
- package/lib/typescript/browserRoutes.d.ts.map +1 -0
- package/lib/typescript/components/ReactNavigationRoutes.web.d.ts +2 -0
- package/lib/typescript/components/ReactNavigationRoutes.web.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +1 -1
- package/lib/typescript/expoRouterStore.web.d.ts +13 -0
- package/lib/typescript/expoRouterStore.web.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/useNavigationStack.web.d.ts +4 -0
- package/lib/typescript/useNavigationStack.web.d.ts.map +1 -0
- package/lib/typescript/utils/routeTemplate.d.ts +13 -0
- package/lib/typescript/utils/routeTemplate.d.ts.map +1 -1
- package/lib/typescript/utils/safeReactNavigation.web.d.ts +22 -0
- package/lib/typescript/utils/safeReactNavigation.web.d.ts.map +1 -0
- package/lib/web/index.mjs +7 -0
- package/package.json +33 -13
package/README.md
CHANGED
|
@@ -74,3 +74,7 @@ Use a Free or Pro Buoy account. History limits and paid features vary by tool; s
|
|
|
74
74
|
📚 [Full docs](https://buoy.gg/buoy/latest/docs/tools/routes) · [All Buoy tools](https://github.com/Buoy-gg/buoy)
|
|
75
75
|
|
|
76
76
|
Proprietary software. © Buoy LLC. [Terms](https://buoy.gg/terms)
|
|
77
|
+
|
|
78
|
+
## Web support (unreleased)
|
|
79
|
+
|
|
80
|
+
Browser History API and hash changes feed the shared route history. Register known paths and the framework router adapter for route selection and navigation. The browser build is available in this checkout and has not been published yet. See the [web setup guide](https://buoy.gg/buoy/latest/docs/web-preview) for registration, dependencies, and browser boundaries.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.RouteTracker = RouteTracker;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _browserRoutes = require("./browserRoutes");
|
|
9
|
+
function RouteTracker() {
|
|
10
|
+
(0, _react.useEffect)(_browserRoutes.startBrowserRouteCapture, []);
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getBrowserRoutePaths = getBrowserRoutePaths;
|
|
7
|
+
exports.getBrowserRoutesUpdatedAt = getBrowserRoutesUpdatedAt;
|
|
8
|
+
exports.registerBrowserRoutes = registerBrowserRoutes;
|
|
9
|
+
const declared = new Map();
|
|
10
|
+
let updatedAt = null;
|
|
11
|
+
function registerBrowserRoutes(paths) {
|
|
12
|
+
for (const path of paths) declared.set(path, (declared.get(path) ?? 0) + 1);
|
|
13
|
+
updatedAt = Date.now();
|
|
14
|
+
let active = true;
|
|
15
|
+
return () => {
|
|
16
|
+
if (!active) return;
|
|
17
|
+
active = false;
|
|
18
|
+
for (const path of paths) {
|
|
19
|
+
const count = (declared.get(path) ?? 1) - 1;
|
|
20
|
+
if (count) declared.set(path, count);else declared.delete(path);
|
|
21
|
+
}
|
|
22
|
+
updatedAt = Date.now();
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function getBrowserRoutePaths() {
|
|
26
|
+
return [...declared.keys()];
|
|
27
|
+
}
|
|
28
|
+
function getBrowserRoutesUpdatedAt() {
|
|
29
|
+
return updatedAt;
|
|
30
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.startBrowserRouteCapture = startBrowserRouteCapture;
|
|
7
|
+
var _utils = require("@buoy-gg/shared-ui/utils");
|
|
8
|
+
var _RouteObserver = require("./RouteObserver");
|
|
9
|
+
var _routeEventStore = require("./stores/routeEventStore");
|
|
10
|
+
var _navigationStackStore = require("./stores/navigationStackStore");
|
|
11
|
+
let leases = 0;
|
|
12
|
+
let stop;
|
|
13
|
+
function startBrowserRouteCapture() {
|
|
14
|
+
if (typeof window === 'undefined') return () => {};
|
|
15
|
+
if (leases++ === 0) {
|
|
16
|
+
_routeEventStore.routeEventStore.hydrateBootCapture();
|
|
17
|
+
let previousPathname;
|
|
18
|
+
let previousTime;
|
|
19
|
+
let lastHref = '';
|
|
20
|
+
const visits = [];
|
|
21
|
+
let cursor = -1;
|
|
22
|
+
const update = change => {
|
|
23
|
+
if (change.href === lastHref && change.kind === 'pop') return;
|
|
24
|
+
if (change.kind === 'replace' && cursor >= 0) visits[cursor] = change.href;else if (change.kind === 'pop') {
|
|
25
|
+
const index = visits.lastIndexOf(change.href, cursor - 1);
|
|
26
|
+
const forward = visits.indexOf(change.href, cursor + 1);
|
|
27
|
+
if (index >= 0) cursor = index;else if (forward >= 0) cursor = forward;else {
|
|
28
|
+
visits.splice(cursor + 1);
|
|
29
|
+
visits.push(change.href);
|
|
30
|
+
cursor++;
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
visits.splice(cursor + 1);
|
|
34
|
+
visits.push(change.href);
|
|
35
|
+
cursor++;
|
|
36
|
+
}
|
|
37
|
+
lastHref = change.href;
|
|
38
|
+
const url = (0, _utils.getBrowserRouteURL)(change.href);
|
|
39
|
+
const timestamp = Date.now();
|
|
40
|
+
_RouteObserver.routeObserver.emit({
|
|
41
|
+
pathname: url.pathname,
|
|
42
|
+
params: Object.fromEntries(url.searchParams),
|
|
43
|
+
segments: url.pathname.split('/').filter(Boolean),
|
|
44
|
+
previousPathname,
|
|
45
|
+
timestamp,
|
|
46
|
+
timeSincePrevious: previousTime === undefined ? undefined : timestamp - previousTime
|
|
47
|
+
});
|
|
48
|
+
previousPathname = url.pathname;
|
|
49
|
+
previousTime = timestamp;
|
|
50
|
+
_navigationStackStore.navigationStackStore.setStack(visits.map((href, index) => {
|
|
51
|
+
const location = (0, _utils.getBrowserRouteURL)(href);
|
|
52
|
+
return {
|
|
53
|
+
key: `browser:${index}`,
|
|
54
|
+
name: location.pathname,
|
|
55
|
+
pathname: location.pathname,
|
|
56
|
+
params: Object.fromEntries(location.searchParams),
|
|
57
|
+
index,
|
|
58
|
+
isFocused: index === cursor,
|
|
59
|
+
canPop: index > 0
|
|
60
|
+
};
|
|
61
|
+
}));
|
|
62
|
+
};
|
|
63
|
+
const goTo = index => {
|
|
64
|
+
if (!Number.isInteger(index) || index < 0 || index >= visits.length) throw new Error('That browser history entry is unavailable.');
|
|
65
|
+
window.history.go(index - cursor);
|
|
66
|
+
};
|
|
67
|
+
_navigationStackStore.navigationStackStore.setActions({
|
|
68
|
+
navigateToIndex: goTo,
|
|
69
|
+
popToIndex: goTo,
|
|
70
|
+
goBack: _utils.browserRouter.back,
|
|
71
|
+
popToTop: () => goTo(0),
|
|
72
|
+
navigateToRoute: (name, params) => {
|
|
73
|
+
_utils.browserRouter.push({
|
|
74
|
+
pathname: name,
|
|
75
|
+
params: params
|
|
76
|
+
});
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
stop = (0, _utils.subscribeBrowserNavigation)(update);
|
|
81
|
+
update({
|
|
82
|
+
href: (0, _utils.getBrowserHref)(),
|
|
83
|
+
kind: 'initial'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
let released = false;
|
|
87
|
+
return () => {
|
|
88
|
+
if (released) return;
|
|
89
|
+
released = true;
|
|
90
|
+
if (--leases === 0) {
|
|
91
|
+
stop?.();
|
|
92
|
+
stop = undefined;
|
|
93
|
+
_navigationStackStore.navigationStackStore.setActions(null);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "ReactNavigationRoutes", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _RoutesSitemap.RoutesSitemap;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
var _RoutesSitemap = require("./RoutesSitemap");
|
|
@@ -40,7 +40,7 @@ function RouteEventDetail({
|
|
|
40
40
|
style: styles.footer,
|
|
41
41
|
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
|
|
42
42
|
style: styles.goButton,
|
|
43
|
-
onPress: () => onNavigate(event.pathname),
|
|
43
|
+
onPress: () => onNavigate((0, _routeTemplate.routeHref)(event.pathname, event.params)),
|
|
44
44
|
activeOpacity: 0.8,
|
|
45
45
|
testID: "buoy-routes-detail-go",
|
|
46
46
|
accessibilityRole: "button",
|
|
@@ -137,7 +137,7 @@ const RouteEventItemCompact = exports.RouteEventItemCompact = /*#__PURE__*/(0, _
|
|
|
137
137
|
if (!onNavigate) return undefined;
|
|
138
138
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
139
139
|
style: styles.goButton,
|
|
140
|
-
onPress: () => onNavigate(event.pathname),
|
|
140
|
+
onPress: () => onNavigate((0, _routeTemplate.routeHref)(event.pathname, event.params)),
|
|
141
141
|
testID: `${testID}-go`,
|
|
142
142
|
accessibilityRole: "button",
|
|
143
143
|
accessibilityLabel: `Go to ${event.pathname}`,
|
|
@@ -18,6 +18,7 @@ var _RoutesSitemap = require("./RoutesSitemap");
|
|
|
18
18
|
var _ReactNavigationRoutes = require("./ReactNavigationRoutes");
|
|
19
19
|
var _NavigationStack = require("./NavigationStack");
|
|
20
20
|
var _RouteEventsTimeline = require("./RouteEventsTimeline");
|
|
21
|
+
var _routeTemplate = require("../utils/routeTemplate");
|
|
21
22
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
22
23
|
// Resolved once at module load; null when React Navigation is absent
|
|
23
24
|
// (useSafeNavigationContainerRef then yields undefined, so the dispatch
|
|
@@ -68,16 +69,23 @@ function findScreenPath(state, screenName) {
|
|
|
68
69
|
* e.g. ["MainApp", "Settings", "SettingScreen"] becomes:
|
|
69
70
|
* navigate("MainApp", { screen: "Settings", params: { screen: "SettingScreen" } })
|
|
70
71
|
*/
|
|
71
|
-
function buildNestedNavigateAction(path) {
|
|
72
|
+
function buildNestedNavigateAction(path, leafParams) {
|
|
72
73
|
if (path.length === 0) return null;
|
|
74
|
+
const leaf = leafParams && Object.keys(leafParams).length > 0 ? leafParams : undefined;
|
|
73
75
|
if (path.length === 1) {
|
|
74
|
-
return CommonActions.navigate({
|
|
76
|
+
return CommonActions.navigate(leaf ? {
|
|
77
|
+
name: path[0],
|
|
78
|
+
params: leaf
|
|
79
|
+
} : {
|
|
75
80
|
name: path[0]
|
|
76
81
|
});
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
// Build nested params from the end
|
|
80
|
-
let params = {
|
|
85
|
+
let params = leaf ? {
|
|
86
|
+
screen: path[path.length - 1],
|
|
87
|
+
params: leaf
|
|
88
|
+
} : {
|
|
81
89
|
screen: path[path.length - 1]
|
|
82
90
|
};
|
|
83
91
|
for (let i = path.length - 2; i >= 1; i--) {
|
|
@@ -175,15 +183,21 @@ function RouteEventsModalWithTabs({
|
|
|
175
183
|
if (hasExpoRouter) {
|
|
176
184
|
router.push(pathname);
|
|
177
185
|
} else if (containerRef) {
|
|
178
|
-
// On RN CLI, pathname is /<ScreenName> — extract the screen name
|
|
179
|
-
|
|
186
|
+
// On RN CLI, pathname is /<ScreenName> — extract the screen name.
|
|
187
|
+
// A recorded event's href may carry its params as a query string.
|
|
188
|
+
const {
|
|
189
|
+
pathname: path,
|
|
190
|
+
params: routeParams
|
|
191
|
+
} = (0, _routeTemplate.splitRouteHref)(pathname);
|
|
192
|
+
const screenName = path.startsWith("/") ? path.slice(1) : path;
|
|
193
|
+
const hasParams = Object.keys(routeParams).length > 0;
|
|
180
194
|
|
|
181
195
|
// Walk the state tree to find the nesting path to this screen
|
|
182
196
|
const rootState = containerRef.getRootState?.();
|
|
183
197
|
if (rootState) {
|
|
184
198
|
const path = findScreenPath(rootState, screenName);
|
|
185
199
|
if (path) {
|
|
186
|
-
const action = buildNestedNavigateAction(path);
|
|
200
|
+
const action = buildNestedNavigateAction(path, routeParams);
|
|
187
201
|
if (action) {
|
|
188
202
|
containerRef.dispatch(action);
|
|
189
203
|
return;
|
|
@@ -192,7 +206,10 @@ function RouteEventsModalWithTabs({
|
|
|
192
206
|
}
|
|
193
207
|
|
|
194
208
|
// Fallback: try direct navigation (works for top-level screens)
|
|
195
|
-
containerRef.dispatch(CommonActions.navigate({
|
|
209
|
+
containerRef.dispatch(CommonActions.navigate(hasParams ? {
|
|
210
|
+
name: screenName,
|
|
211
|
+
params: routeParams
|
|
212
|
+
} : {
|
|
196
213
|
name: screenName
|
|
197
214
|
}));
|
|
198
215
|
} else {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getExpoRouterStore = getExpoRouterStore;
|
|
7
|
+
exports.getRouteNodeMetadata = getRouteNodeMetadata;
|
|
8
|
+
exports.isExpoRouterStoreSupported = isExpoRouterStoreSupported;
|
|
9
|
+
exports.loadRouteNode = loadRouteNode;
|
|
10
|
+
var _utils = require("@buoy-gg/shared-ui/utils");
|
|
11
|
+
var _browserRouteRegistry = require("./browserRouteRegistry");
|
|
12
|
+
function isExpoRouterStoreSupported() {
|
|
13
|
+
return typeof window !== 'undefined';
|
|
14
|
+
}
|
|
15
|
+
function loadRouteNode() {
|
|
16
|
+
if (!isExpoRouterStoreSupported()) return null;
|
|
17
|
+
const paths = new Set([...(0, _browserRouteRegistry.getBrowserRoutePaths)(), new URL((0, _utils.getBrowserHref)()).pathname]);
|
|
18
|
+
return {
|
|
19
|
+
route: '',
|
|
20
|
+
type: 'layout',
|
|
21
|
+
contextKey: 'browser',
|
|
22
|
+
children: [...paths].map(path => ({
|
|
23
|
+
route: path.replace(/^\//, '') || 'index',
|
|
24
|
+
type: 'route',
|
|
25
|
+
contextKey: path,
|
|
26
|
+
children: [],
|
|
27
|
+
dynamic: null
|
|
28
|
+
}))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function getExpoRouterStore() {
|
|
32
|
+
return {
|
|
33
|
+
routeNode: loadRouteNode(),
|
|
34
|
+
navigationRef: {
|
|
35
|
+
isReady: () => typeof window !== 'undefined'
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function getRouteNodeMetadata() {
|
|
40
|
+
return {
|
|
41
|
+
source: null,
|
|
42
|
+
lastLoadedAt: (0, _browserRouteRegistry.getBrowserRoutesUpdatedAt)()
|
|
43
|
+
};
|
|
44
|
+
}
|
package/lib/commonjs/index.js
CHANGED
|
@@ -75,6 +75,12 @@ Object.defineProperty(exports, "navigationStackStore", {
|
|
|
75
75
|
return _navigationStackStore.navigationStackStore;
|
|
76
76
|
}
|
|
77
77
|
});
|
|
78
|
+
Object.defineProperty(exports, "registerBrowserRoutes", {
|
|
79
|
+
enumerable: true,
|
|
80
|
+
get: function () {
|
|
81
|
+
return _browserRouteRegistry.registerBrowserRoutes;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
78
84
|
Object.defineProperty(exports, "routeEventStore", {
|
|
79
85
|
enumerable: true,
|
|
80
86
|
get: function () {
|
|
@@ -144,6 +150,7 @@ Object.defineProperty(exports, "useRouteSitemap", {
|
|
|
144
150
|
var _preset = require("./preset");
|
|
145
151
|
var _RouteEventsModalWithTabs = require("./components/RouteEventsModalWithTabs");
|
|
146
152
|
var _RouteTracker = require("./RouteTracker");
|
|
153
|
+
var _browserRouteRegistry = require("./browserRouteRegistry");
|
|
147
154
|
var _RoutesSitemap = require("./components/RoutesSitemap");
|
|
148
155
|
var _NavigationStack = require("./components/NavigationStack");
|
|
149
156
|
var _RouteEventItemCompact = require("./components/RouteEventItemCompact");
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useNavigationStack = useNavigationStack;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _navigationStackStore = require("./stores/navigationStackStore");
|
|
9
|
+
const empty = [];
|
|
10
|
+
function useNavigationStack() {
|
|
11
|
+
const stack = (0, _react.useSyncExternalStore)(callback => _navigationStackStore.navigationStackStore.subscribe(callback), () => _navigationStackStore.navigationStackStore.getStack(), () => empty);
|
|
12
|
+
const focused = stack.find(item => item.isFocused) ?? null;
|
|
13
|
+
const actions = () => {
|
|
14
|
+
const current = _navigationStackStore.navigationStackStore.getActions();
|
|
15
|
+
if (!current) throw new Error('Mount RouteTracker to capture browser navigation.');
|
|
16
|
+
return current;
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
stack,
|
|
20
|
+
focusedRoute: focused,
|
|
21
|
+
stackDepth: stack.length,
|
|
22
|
+
isAtRoot: !focused || focused.index === 0,
|
|
23
|
+
isLoaded: stack.length > 0,
|
|
24
|
+
error: null,
|
|
25
|
+
refresh: () => {},
|
|
26
|
+
navigateToIndex: index => actions().navigateToIndex(index),
|
|
27
|
+
navigateToRoute: (name, params) => actions().navigateToRoute?.(name, params) ?? false,
|
|
28
|
+
popToIndex: index => actions().popToIndex(index),
|
|
29
|
+
goBack: () => actions().goBack(),
|
|
30
|
+
popToTop: () => actions().popToTop()
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.getRouteTemplate = getRouteTemplate;
|
|
7
|
+
exports.routeHref = routeHref;
|
|
8
|
+
exports.splitRouteHref = splitRouteHref;
|
|
7
9
|
/**
|
|
8
10
|
* Infer a route template from a concrete pathname and its segments.
|
|
9
11
|
* Dynamic segments (numeric ids, UUIDs) collapse to `[id]`, so
|
|
@@ -25,4 +27,53 @@ function getRouteTemplate(pathname, segments) {
|
|
|
25
27
|
});
|
|
26
28
|
const template = "/" + templateParts.join("/");
|
|
27
29
|
return template !== pathname ? template : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The href that reopens a recorded route: its pathname plus the params that are
|
|
34
|
+
* not already in the path, as a query string. Expo Router reports dynamic
|
|
35
|
+
* segment values (`/order/42` → `{ id: "42" }`) and search params (`?menu=1`)
|
|
36
|
+
* in one `params` object; only the second kind needs to be carried, because the
|
|
37
|
+
* first is already in the pathname.
|
|
38
|
+
*/
|
|
39
|
+
function routeHref(pathname, params) {
|
|
40
|
+
if (!params) return pathname;
|
|
41
|
+
const inPath = new Set(pathname.split("/").filter(Boolean).map(seg => {
|
|
42
|
+
try {
|
|
43
|
+
return decodeURIComponent(seg);
|
|
44
|
+
} catch {
|
|
45
|
+
return seg;
|
|
46
|
+
}
|
|
47
|
+
}));
|
|
48
|
+
const query = [];
|
|
49
|
+
for (const [key, raw] of Object.entries(params)) {
|
|
50
|
+
const values = Array.isArray(raw) ? raw : [raw];
|
|
51
|
+
// A catch-all segment's values, or one dynamic segment's value.
|
|
52
|
+
if (values.length > 0 && values.every(v => inPath.has(String(v)))) continue;
|
|
53
|
+
for (const v of values) query.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`);
|
|
54
|
+
}
|
|
55
|
+
return query.length ? `${pathname}?${query.join("&")}` : pathname;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Split an href from {@link routeHref} back into its path and params. */
|
|
59
|
+
function splitRouteHref(href) {
|
|
60
|
+
const q = href.indexOf("?");
|
|
61
|
+
if (q < 0) return {
|
|
62
|
+
pathname: href,
|
|
63
|
+
params: {}
|
|
64
|
+
};
|
|
65
|
+
const params = {};
|
|
66
|
+
for (const pair of href.slice(q + 1).split("&")) {
|
|
67
|
+
if (!pair) continue;
|
|
68
|
+
const [k, v = ""] = pair.split("=");
|
|
69
|
+
try {
|
|
70
|
+
params[decodeURIComponent(k)] = decodeURIComponent(v);
|
|
71
|
+
} catch {
|
|
72
|
+
params[k] = v;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
pathname: href.slice(0, q),
|
|
77
|
+
params
|
|
78
|
+
};
|
|
28
79
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getCommonActions = getCommonActions;
|
|
7
|
+
exports.getStackActions = getStackActions;
|
|
8
|
+
exports.isReactNavigationAvailable = isReactNavigationAvailable;
|
|
9
|
+
exports.useSafeNavigation = useSafeNavigation;
|
|
10
|
+
exports.useSafeNavigationContainerRef = useSafeNavigationContainerRef;
|
|
11
|
+
exports.useSafeNavigationState = useSafeNavigationState;
|
|
12
|
+
var _utils = require("@buoy-gg/shared-ui/utils");
|
|
13
|
+
function useSafeNavigation() {
|
|
14
|
+
return _utils.browserRouter;
|
|
15
|
+
}
|
|
16
|
+
function useSafeNavigationState(selector) {
|
|
17
|
+
return selector({
|
|
18
|
+
routes: [],
|
|
19
|
+
routeNames: [],
|
|
20
|
+
index: 0,
|
|
21
|
+
key: 'browser',
|
|
22
|
+
type: 'stack'
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function useSafeNavigationContainerRef() {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
function getCommonActions() {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function getStackActions() {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function isReactNavigationAvailable() {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const declared = new Map();
|
|
4
|
+
let updatedAt = null;
|
|
5
|
+
export function registerBrowserRoutes(paths) {
|
|
6
|
+
for (const path of paths) declared.set(path, (declared.get(path) ?? 0) + 1);
|
|
7
|
+
updatedAt = Date.now();
|
|
8
|
+
let active = true;
|
|
9
|
+
return () => {
|
|
10
|
+
if (!active) return;
|
|
11
|
+
active = false;
|
|
12
|
+
for (const path of paths) {
|
|
13
|
+
const count = (declared.get(path) ?? 1) - 1;
|
|
14
|
+
if (count) declared.set(path, count);else declared.delete(path);
|
|
15
|
+
}
|
|
16
|
+
updatedAt = Date.now();
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function getBrowserRoutePaths() {
|
|
20
|
+
return [...declared.keys()];
|
|
21
|
+
}
|
|
22
|
+
export function getBrowserRoutesUpdatedAt() {
|
|
23
|
+
return updatedAt;
|
|
24
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { browserRouter, getBrowserHref, getBrowserRouteURL, subscribeBrowserNavigation } from '@buoy-gg/shared-ui/utils';
|
|
4
|
+
import { routeObserver } from './RouteObserver';
|
|
5
|
+
import { routeEventStore } from './stores/routeEventStore';
|
|
6
|
+
import { navigationStackStore } from './stores/navigationStackStore';
|
|
7
|
+
let leases = 0;
|
|
8
|
+
let stop;
|
|
9
|
+
export function startBrowserRouteCapture() {
|
|
10
|
+
if (typeof window === 'undefined') return () => {};
|
|
11
|
+
if (leases++ === 0) {
|
|
12
|
+
routeEventStore.hydrateBootCapture();
|
|
13
|
+
let previousPathname;
|
|
14
|
+
let previousTime;
|
|
15
|
+
let lastHref = '';
|
|
16
|
+
const visits = [];
|
|
17
|
+
let cursor = -1;
|
|
18
|
+
const update = change => {
|
|
19
|
+
if (change.href === lastHref && change.kind === 'pop') return;
|
|
20
|
+
if (change.kind === 'replace' && cursor >= 0) visits[cursor] = change.href;else if (change.kind === 'pop') {
|
|
21
|
+
const index = visits.lastIndexOf(change.href, cursor - 1);
|
|
22
|
+
const forward = visits.indexOf(change.href, cursor + 1);
|
|
23
|
+
if (index >= 0) cursor = index;else if (forward >= 0) cursor = forward;else {
|
|
24
|
+
visits.splice(cursor + 1);
|
|
25
|
+
visits.push(change.href);
|
|
26
|
+
cursor++;
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
visits.splice(cursor + 1);
|
|
30
|
+
visits.push(change.href);
|
|
31
|
+
cursor++;
|
|
32
|
+
}
|
|
33
|
+
lastHref = change.href;
|
|
34
|
+
const url = getBrowserRouteURL(change.href);
|
|
35
|
+
const timestamp = Date.now();
|
|
36
|
+
routeObserver.emit({
|
|
37
|
+
pathname: url.pathname,
|
|
38
|
+
params: Object.fromEntries(url.searchParams),
|
|
39
|
+
segments: url.pathname.split('/').filter(Boolean),
|
|
40
|
+
previousPathname,
|
|
41
|
+
timestamp,
|
|
42
|
+
timeSincePrevious: previousTime === undefined ? undefined : timestamp - previousTime
|
|
43
|
+
});
|
|
44
|
+
previousPathname = url.pathname;
|
|
45
|
+
previousTime = timestamp;
|
|
46
|
+
navigationStackStore.setStack(visits.map((href, index) => {
|
|
47
|
+
const location = getBrowserRouteURL(href);
|
|
48
|
+
return {
|
|
49
|
+
key: `browser:${index}`,
|
|
50
|
+
name: location.pathname,
|
|
51
|
+
pathname: location.pathname,
|
|
52
|
+
params: Object.fromEntries(location.searchParams),
|
|
53
|
+
index,
|
|
54
|
+
isFocused: index === cursor,
|
|
55
|
+
canPop: index > 0
|
|
56
|
+
};
|
|
57
|
+
}));
|
|
58
|
+
};
|
|
59
|
+
const goTo = index => {
|
|
60
|
+
if (!Number.isInteger(index) || index < 0 || index >= visits.length) throw new Error('That browser history entry is unavailable.');
|
|
61
|
+
window.history.go(index - cursor);
|
|
62
|
+
};
|
|
63
|
+
navigationStackStore.setActions({
|
|
64
|
+
navigateToIndex: goTo,
|
|
65
|
+
popToIndex: goTo,
|
|
66
|
+
goBack: browserRouter.back,
|
|
67
|
+
popToTop: () => goTo(0),
|
|
68
|
+
navigateToRoute: (name, params) => {
|
|
69
|
+
browserRouter.push({
|
|
70
|
+
pathname: name,
|
|
71
|
+
params: params
|
|
72
|
+
});
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
stop = subscribeBrowserNavigation(update);
|
|
77
|
+
update({
|
|
78
|
+
href: getBrowserHref(),
|
|
79
|
+
kind: 'initial'
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
let released = false;
|
|
83
|
+
return () => {
|
|
84
|
+
if (released) return;
|
|
85
|
+
released = true;
|
|
86
|
+
if (--leases === 0) {
|
|
87
|
+
stop?.();
|
|
88
|
+
stop = undefined;
|
|
89
|
+
navigationStackStore.setActions(null);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from "react-native";
|
|
13
13
|
import { Navigation, night, monoFont } from "@buoy-gg/shared-ui";
|
|
14
14
|
import { RouteEventExpandedContent } from "./RouteEventExpandedContent";
|
|
15
|
-
import { getRouteTemplate } from "../utils/routeTemplate";
|
|
15
|
+
import { getRouteTemplate, routeHref } from "../utils/routeTemplate";
|
|
16
16
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
17
17
|
export function RouteEventDetail({
|
|
18
18
|
event,
|
|
@@ -36,7 +36,7 @@ export function RouteEventDetail({
|
|
|
36
36
|
style: styles.footer,
|
|
37
37
|
children: /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
38
38
|
style: styles.goButton,
|
|
39
|
-
onPress: () => onNavigate(event.pathname),
|
|
39
|
+
onPress: () => onNavigate(routeHref(event.pathname, event.params)),
|
|
40
40
|
activeOpacity: 0.8,
|
|
41
41
|
testID: "buoy-routes-detail-go",
|
|
42
42
|
accessibilityRole: "button",
|
|
@@ -10,7 +10,7 @@ import { memo, useMemo } from "react";
|
|
|
10
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
|
-
import { getRouteTemplate } from "../utils/routeTemplate";
|
|
13
|
+
import { getRouteTemplate, routeHref } from "../utils/routeTemplate";
|
|
14
14
|
import { eventRowTestID } from "../utils/testIDs";
|
|
15
15
|
|
|
16
16
|
// Route type for color coding
|
|
@@ -133,7 +133,7 @@ export const RouteEventItemCompact = /*#__PURE__*/memo(function RouteEventItemCo
|
|
|
133
133
|
if (!onNavigate) return undefined;
|
|
134
134
|
return /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
135
135
|
style: styles.goButton,
|
|
136
|
-
onPress: () => onNavigate(event.pathname),
|
|
136
|
+
onPress: () => onNavigate(routeHref(event.pathname, event.params)),
|
|
137
137
|
testID: `${testID}-go`,
|
|
138
138
|
accessibilityRole: "button",
|
|
139
139
|
accessibilityLabel: `Go to ${event.pathname}`,
|