@rnzeus/atlas 0.1.0
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/dist/README.md +21 -0
- package/dist/config/internal.d.ts +1 -0
- package/dist/config/internal.js +1 -0
- package/dist/config/navigation/index.d.ts +1 -0
- package/dist/config/navigation/index.js +1 -0
- package/dist/config/navigation/types.d.ts +55 -0
- package/dist/config/navigation/types.js +1 -0
- package/dist/config/navigation.types.d.ts +25 -0
- package/dist/config/navigation.types.js +1 -0
- package/dist/hooks/README.md +11 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/internal.d.ts +1 -0
- package/dist/hooks/internal.js +1 -0
- package/dist/hooks/use-path-navigation/README.md +66 -0
- package/dist/hooks/use-path-navigation/hook.d.ts +119 -0
- package/dist/hooks/use-path-navigation/hook.js +52 -0
- package/dist/hooks/use-path-navigation/index.d.ts +1 -0
- package/dist/hooks/use-path-navigation/index.js +1 -0
- package/dist/services/README.md +17 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +3 -0
- package/dist/services/internal.d.ts +1 -0
- package/dist/services/internal.js +1 -0
- package/dist/services/navigation/README.md +131 -0
- package/dist/services/navigation/api.d.ts +5 -0
- package/dist/services/navigation/api.js +11 -0
- package/dist/services/navigation/chain-builder.d.ts +16 -0
- package/dist/services/navigation/chain-builder.js +31 -0
- package/dist/services/navigation/index.d.ts +3 -0
- package/dist/services/navigation/index.js +3 -0
- package/dist/services/navigation/service.d.ts +55 -0
- package/dist/services/navigation/service.js +536 -0
- package/dist/services/navigation/types.d.ts +13 -0
- package/dist/services/navigation/types.js +1 -0
- package/dist/services/navigation/zod-adapter.d.ts +22 -0
- package/dist/services/navigation/zod-adapter.js +36 -0
- package/dist/services/transport/README.md +79 -0
- package/dist/services/transport/build-query.d.ts +2 -0
- package/dist/services/transport/build-query.js +3 -0
- package/dist/services/transport/create-transport-methods.d.ts +8 -0
- package/dist/services/transport/create-transport-methods.js +34 -0
- package/dist/services/transport/error-exception.d.ts +8 -0
- package/dist/services/transport/error-exception.js +16 -0
- package/dist/services/transport/index.d.ts +3 -0
- package/dist/services/transport/index.js +2 -0
- package/dist/services/transport/read-response-body.d.ts +3 -0
- package/dist/services/transport/read-response-body.js +27 -0
- package/dist/services/transport/service.d.ts +22 -0
- package/dist/services/transport/service.js +102 -0
- package/dist/services/transport/types.d.ts +4 -0
- package/dist/services/transport/types.js +1 -0
- package/dist/ui/README.md +17 -0
- package/dist/ui/debug-dock/README.md +58 -0
- package/dist/ui/debug-dock/debug-dock.d.ts +13 -0
- package/dist/ui/debug-dock/debug-dock.js +20 -0
- package/dist/ui/debug-dock/index.d.ts +2 -0
- package/dist/ui/debug-dock/index.js +1 -0
- package/dist/ui/debug-dock/styles.d.ts +8 -0
- package/dist/ui/debug-dock/styles.js +25 -0
- package/dist/ui/debug-dock/use-debug-dock.d.ts +20 -0
- package/dist/ui/debug-dock/use-debug-dock.js +71 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/toast/README.md +62 -0
- package/dist/ui/toast/context.d.ts +7 -0
- package/dist/ui/toast/context.js +6 -0
- package/dist/ui/toast/index.d.ts +5 -0
- package/dist/ui/toast/index.js +2 -0
- package/dist/ui/toast/toast-plate/README.md +38 -0
- package/dist/ui/toast/toast-plate/index.d.ts +2 -0
- package/dist/ui/toast/toast-plate/index.js +1 -0
- package/dist/ui/toast/toast-plate/toast-plate.d.ts +9 -0
- package/dist/ui/toast/toast-plate/toast-plate.js +11 -0
- package/dist/ui/toast/toast-plate/use-toast-plate.d.ts +4 -0
- package/dist/ui/toast/toast-plate/use-toast-plate.js +33 -0
- package/dist/ui/toast/toast-provider.d.ts +10 -0
- package/dist/ui/toast/toast-provider.js +53 -0
- package/dist/ui/toast/toast-viewport/README.md +38 -0
- package/dist/ui/toast/toast-viewport/index.d.ts +2 -0
- package/dist/ui/toast/toast-viewport/index.js +1 -0
- package/dist/ui/toast/toast-viewport/styles.d.ts +5 -0
- package/dist/ui/toast/toast-viewport/styles.js +10 -0
- package/dist/ui/toast/toast-viewport/toast-viewport.d.ts +10 -0
- package/dist/ui/toast/toast-viewport/toast-viewport.js +13 -0
- package/dist/ui/toast/types.d.ts +25 -0
- package/dist/ui/toast/types.js +1 -0
- package/dist/ui/toast/use-toast.d.ts +1 -0
- package/dist/ui/toast/use-toast.js +5 -0
- package/dist/utils/README.md +23 -0
- package/dist/utils/create-typography/README.md +71 -0
- package/dist/utils/create-typography/helper.d.ts +23 -0
- package/dist/utils/create-typography/helper.js +72 -0
- package/dist/utils/create-typography/index.d.ts +1 -0
- package/dist/utils/create-typography/index.js +1 -0
- package/dist/utils/define-route/README.md +50 -0
- package/dist/utils/define-route/helper.d.ts +2 -0
- package/dist/utils/define-route/helper.js +1 -0
- package/dist/utils/define-route/index.d.ts +1 -0
- package/dist/utils/define-route/index.js +1 -0
- package/dist/utils/display-name/README.md +46 -0
- package/dist/utils/display-name/helper.d.ts +2 -0
- package/dist/utils/display-name/helper.js +91 -0
- package/dist/utils/display-name/index.d.ts +1 -0
- package/dist/utils/display-name/index.js +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/internal.d.ts +3 -0
- package/dist/utils/internal.js +3 -0
- package/package.json +74 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { LinkingOptions, NavigationContainerRef, ParamListBase } from '@react-navigation/native';
|
|
2
|
+
import type { BuiltPath, NavigationOnError, NavigationStrict, QueryOptions, RouteDef, RouteParams, RouteQuery } from '../../config/internal';
|
|
3
|
+
import type { NavLike } from './types';
|
|
4
|
+
declare class NavService<T extends ParamListBase = ParamListBase> {
|
|
5
|
+
private ref?;
|
|
6
|
+
private linking?;
|
|
7
|
+
/** strict mode (boolean) */
|
|
8
|
+
strict: NavigationStrict;
|
|
9
|
+
/** error handler (optional) */
|
|
10
|
+
onError?: NavigationOnError;
|
|
11
|
+
set navRef(ref: NavigationContainerRef<T>);
|
|
12
|
+
set linkingConfig(linking: LinkingOptions<T>);
|
|
13
|
+
get navRef(): NavigationContainerRef<T>;
|
|
14
|
+
private meta;
|
|
15
|
+
private brandPath;
|
|
16
|
+
private getRouteFromPath;
|
|
17
|
+
private handleValidationError;
|
|
18
|
+
private normalizeBuild;
|
|
19
|
+
private normalizeQuery;
|
|
20
|
+
private ensureRefReady;
|
|
21
|
+
private ensureLinking;
|
|
22
|
+
private last;
|
|
23
|
+
private getLeafFromState;
|
|
24
|
+
private parsePath;
|
|
25
|
+
private buildNestedNavigate;
|
|
26
|
+
private ensureAtParents;
|
|
27
|
+
private findNavigatorKeyByName;
|
|
28
|
+
private getStackStateByName;
|
|
29
|
+
private getCurrentStackKey;
|
|
30
|
+
private pushOrReplace;
|
|
31
|
+
private hasRoute;
|
|
32
|
+
private routeTreeHasLeaf;
|
|
33
|
+
private buildParamsFromChain;
|
|
34
|
+
navigate: (path: BuiltPath, navigation?: NavLike) => void;
|
|
35
|
+
push: (path: BuiltPath) => void;
|
|
36
|
+
replace: (path: BuiltPath) => void;
|
|
37
|
+
popTo: (path: BuiltPath, navigation?: NavLike) => void;
|
|
38
|
+
navigateUnsafe: (path: string, navigation?: NavLike) => void;
|
|
39
|
+
pushUnsafe: (path: string) => void;
|
|
40
|
+
replaceUnsafe: (path: string) => void;
|
|
41
|
+
popToTop: (navigation?: NavLike) => void;
|
|
42
|
+
popToUnsafe: (path: string, navigation?: NavLike) => void;
|
|
43
|
+
goBack: () => void;
|
|
44
|
+
reset: (path: BuiltPath) => void;
|
|
45
|
+
resetUnsafe: (path: string) => void;
|
|
46
|
+
getCurrentPath: () => string;
|
|
47
|
+
build<R extends RouteDef<undefined, any>>(route: R): BuiltPath<R>;
|
|
48
|
+
build<R extends RouteDef<Record<string, string | number>, any>>(route: R, params: RouteParams<R>): BuiltPath<R>;
|
|
49
|
+
private safeJsonStringify;
|
|
50
|
+
query<R extends RouteDef<any, undefined>>(url: BuiltPath<R>): BuiltPath<R>;
|
|
51
|
+
query<R extends RouteDef<any, undefined>>(url: BuiltPath<R>, q: undefined, opts?: QueryOptions): BuiltPath<R>;
|
|
52
|
+
query<R extends RouteDef<any, any>>(url: BuiltPath<R>, q: RouteQuery<R>, opts?: QueryOptions): BuiltPath<R>;
|
|
53
|
+
}
|
|
54
|
+
declare const _default: NavService<ParamListBase>;
|
|
55
|
+
export default _default;
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { CommonActions, getPathFromState, getStateFromPath, StackActions } from '@react-navigation/native';
|
|
2
|
+
class NavService {
|
|
3
|
+
constructor() {
|
|
4
|
+
/** strict mode (boolean) */
|
|
5
|
+
this.strict = false;
|
|
6
|
+
// ───────────────── strict helpers ─────────────────
|
|
7
|
+
this.meta = new WeakMap();
|
|
8
|
+
// ───────────── public API (safe) ─────────────
|
|
9
|
+
this.navigate = (path, navigation) => {
|
|
10
|
+
this.navigateUnsafe(path, navigation);
|
|
11
|
+
};
|
|
12
|
+
this.push = (path) => {
|
|
13
|
+
this.pushUnsafe(path);
|
|
14
|
+
};
|
|
15
|
+
this.replace = (path) => {
|
|
16
|
+
this.replaceUnsafe(path);
|
|
17
|
+
};
|
|
18
|
+
this.popTo = (path, navigation) => {
|
|
19
|
+
this.popToUnsafe(path, navigation);
|
|
20
|
+
};
|
|
21
|
+
// ───────────── public API (unsafe) ─────────────
|
|
22
|
+
this.navigateUnsafe = (path, navigation) => {
|
|
23
|
+
if (!this.ensureLinking())
|
|
24
|
+
return;
|
|
25
|
+
if (navigation) {
|
|
26
|
+
const parsed = this.parsePath(path);
|
|
27
|
+
if (!parsed)
|
|
28
|
+
return;
|
|
29
|
+
const { parents, leaf } = parsed;
|
|
30
|
+
navigation.dispatch(this.buildNestedNavigate(parents, leaf));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!this.ensureRefReady())
|
|
34
|
+
return;
|
|
35
|
+
const parsed = this.parsePath(path);
|
|
36
|
+
if (!parsed)
|
|
37
|
+
return;
|
|
38
|
+
const { parents, leaf } = parsed;
|
|
39
|
+
this.ensureAtParents(parents);
|
|
40
|
+
const parent = this.last(parents);
|
|
41
|
+
if (!parent)
|
|
42
|
+
return;
|
|
43
|
+
const targetKey = this.findNavigatorKeyByName(parent);
|
|
44
|
+
const stackState = this.getStackStateByName(parent);
|
|
45
|
+
if (!targetKey || !stackState) {
|
|
46
|
+
this.ref?.dispatch(this.buildNestedNavigate(parents, leaf));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const routes = stackState.routes ?? [];
|
|
50
|
+
const existingIndex = routes.findIndex(r => r.name === leaf.name);
|
|
51
|
+
if (existingIndex >= 0) {
|
|
52
|
+
const count = routes.length - 1 - existingIndex;
|
|
53
|
+
if (count > 0)
|
|
54
|
+
this.ref?.dispatch({ ...StackActions.pop(count), target: targetKey });
|
|
55
|
+
if (leaf.params && Object.keys(leaf.params).length > 0) {
|
|
56
|
+
this.ref?.dispatch(CommonActions.setParams({
|
|
57
|
+
params: leaf.params,
|
|
58
|
+
target: routes[existingIndex].key,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
this.ref?.dispatch({ ...StackActions.push(leaf.name, leaf.params), target: targetKey });
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
this.pushUnsafe = (path) => {
|
|
67
|
+
this.pushOrReplace('push', path);
|
|
68
|
+
};
|
|
69
|
+
this.replaceUnsafe = (path) => {
|
|
70
|
+
this.pushOrReplace('replace', path);
|
|
71
|
+
};
|
|
72
|
+
this.popToTop = (navigation) => {
|
|
73
|
+
if (navigation) {
|
|
74
|
+
navigation.dispatch(StackActions.popToTop());
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!this.ensureRefReady())
|
|
78
|
+
return;
|
|
79
|
+
const targetKey = this.getCurrentStackKey();
|
|
80
|
+
if (!targetKey)
|
|
81
|
+
return;
|
|
82
|
+
this.ref?.dispatch({ ...StackActions.popToTop(), target: targetKey });
|
|
83
|
+
};
|
|
84
|
+
this.popToUnsafe = (path, navigation) => {
|
|
85
|
+
if (!this.ensureLinking())
|
|
86
|
+
return;
|
|
87
|
+
const parsed = this.parsePath(path);
|
|
88
|
+
if (!parsed)
|
|
89
|
+
return;
|
|
90
|
+
const { parents, leaf } = parsed;
|
|
91
|
+
const targetChain = [...parents, leaf.name];
|
|
92
|
+
if (navigation) {
|
|
93
|
+
const navs = [];
|
|
94
|
+
let n = navigation;
|
|
95
|
+
while (n) {
|
|
96
|
+
navs.push(n);
|
|
97
|
+
n = n.getParent?.();
|
|
98
|
+
if (!n)
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
const navChain = [...navs].reverse();
|
|
102
|
+
const activeState = navChain[navChain.length - 1]?.getState?.();
|
|
103
|
+
if (this.hasRoute(activeState, leaf.name)) {
|
|
104
|
+
const deepNav = navChain[navChain.length - 1];
|
|
105
|
+
if (typeof deepNav.popTo === 'function') {
|
|
106
|
+
deepNav.popTo(leaf.name);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const routes = activeState?.routes ?? [];
|
|
110
|
+
const idx = routes.findIndex(r => r.name === leaf.name);
|
|
111
|
+
const count = idx >= 0 ? routes.length - 1 - idx : 0;
|
|
112
|
+
if (count > 0)
|
|
113
|
+
navigation.dispatch(StackActions.pop(count));
|
|
114
|
+
if (leaf.params && Object.keys(leaf.params).length) {
|
|
115
|
+
navigation.dispatch(CommonActions.setParams({ params: leaf.params }));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
let chosenLevel = -1;
|
|
121
|
+
for (let j = targetChain.length - 1; j >= 0; j--) {
|
|
122
|
+
const navAtLevel = navChain[j];
|
|
123
|
+
const stateAtLevel = navAtLevel?.getState?.();
|
|
124
|
+
const targetName = targetChain[j];
|
|
125
|
+
if (targetName && this.hasRoute(stateAtLevel, targetName)) {
|
|
126
|
+
chosenLevel = j;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (chosenLevel >= 0) {
|
|
131
|
+
const navAtLevel = navChain[chosenLevel];
|
|
132
|
+
const stateAtLevel = navAtLevel?.getState?.();
|
|
133
|
+
const routes = stateAtLevel?.routes ?? [];
|
|
134
|
+
const idx = routes.findIndex(r => r.name === targetChain[chosenLevel]);
|
|
135
|
+
const count = idx >= 0 ? routes.length - 1 - idx : 0;
|
|
136
|
+
if (count > 0)
|
|
137
|
+
navAtLevel.dispatch(StackActions.pop(count));
|
|
138
|
+
const remainder = targetChain.slice(chosenLevel + 1, targetChain.length - 1);
|
|
139
|
+
const nestedParams = this.buildParamsFromChain(remainder, leaf);
|
|
140
|
+
if (typeof navAtLevel.popTo === 'function') {
|
|
141
|
+
navAtLevel.popTo(targetChain[chosenLevel], nestedParams);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
navAtLevel.dispatch(CommonActions.navigate({ name: targetChain[chosenLevel] || '', params: nestedParams }));
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
navigation.dispatch(this.buildNestedNavigate(parents, leaf));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (!this.ensureRefReady())
|
|
152
|
+
return;
|
|
153
|
+
const chain = [];
|
|
154
|
+
let s = this.ref?.getRootState();
|
|
155
|
+
let k = s?.key;
|
|
156
|
+
while (s) {
|
|
157
|
+
chain.push({ state: s, key: k });
|
|
158
|
+
const r = s.routes?.[s.index ?? 0];
|
|
159
|
+
if (!r?.state)
|
|
160
|
+
break;
|
|
161
|
+
s = r.state;
|
|
162
|
+
k = s?.key ?? k;
|
|
163
|
+
}
|
|
164
|
+
const deepest = chain[chain.length - 1];
|
|
165
|
+
if (this.hasRoute(deepest?.state, leaf.name)) {
|
|
166
|
+
const routes = deepest?.state?.routes ?? [];
|
|
167
|
+
const idx = routes.findIndex(r => r.name === leaf.name);
|
|
168
|
+
const count = idx >= 0 ? routes.length - 1 - idx : 0;
|
|
169
|
+
if (count > 0 && deepest?.key) {
|
|
170
|
+
this.ref?.dispatch({ ...StackActions.pop(count), target: deepest.key });
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
let chosenLevel = -1;
|
|
175
|
+
for (let j = targetChain.length - 1; j >= 0; j--) {
|
|
176
|
+
const st = chain[j]?.state;
|
|
177
|
+
const targetName = targetChain[j];
|
|
178
|
+
if (targetName && this.hasRoute(st, targetName)) {
|
|
179
|
+
chosenLevel = j;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (chosenLevel >= 0) {
|
|
184
|
+
const level = chain[chosenLevel];
|
|
185
|
+
const routes = level?.state?.routes ?? [];
|
|
186
|
+
const idx = routes.findIndex(r => r.name === targetChain[chosenLevel]);
|
|
187
|
+
const count = idx >= 0 ? routes.length - 1 - idx : 0;
|
|
188
|
+
if (count > 0 && level?.key) {
|
|
189
|
+
this.ref?.dispatch({ ...StackActions.pop(count), target: level.key });
|
|
190
|
+
}
|
|
191
|
+
const routeObj = routes[idx];
|
|
192
|
+
if (this.routeTreeHasLeaf(routeObj, leaf.name))
|
|
193
|
+
return;
|
|
194
|
+
const remainder = targetChain.slice(chosenLevel + 1, targetChain.length - 1);
|
|
195
|
+
const nestedParams = this.buildParamsFromChain(remainder, leaf);
|
|
196
|
+
this.ref?.dispatch(CommonActions.navigate({
|
|
197
|
+
name: targetChain[chosenLevel] || '',
|
|
198
|
+
params: nestedParams,
|
|
199
|
+
}));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
this.ref?.dispatch(this.buildNestedNavigate(parents, leaf));
|
|
203
|
+
};
|
|
204
|
+
this.goBack = () => {
|
|
205
|
+
if (!this.ensureRefReady())
|
|
206
|
+
return;
|
|
207
|
+
if (this.ref?.canGoBack?.())
|
|
208
|
+
this.ref.goBack();
|
|
209
|
+
};
|
|
210
|
+
this.reset = (path) => {
|
|
211
|
+
this.resetUnsafe(path);
|
|
212
|
+
};
|
|
213
|
+
this.resetUnsafe = (path) => {
|
|
214
|
+
if (!this.ensureRefReady() || !this.ensureLinking())
|
|
215
|
+
return;
|
|
216
|
+
const opts = this.linking?.config;
|
|
217
|
+
const state = getStateFromPath(path, opts);
|
|
218
|
+
if (state)
|
|
219
|
+
this.ref?.reset(state);
|
|
220
|
+
};
|
|
221
|
+
this.getCurrentPath = () => {
|
|
222
|
+
const state = this.ref?.getRootState();
|
|
223
|
+
if (!this.ensureRefReady() || !this.ensureLinking() || !state)
|
|
224
|
+
return '';
|
|
225
|
+
const opts = this.linking?.config;
|
|
226
|
+
return getPathFromState(state, opts) ?? '';
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
set navRef(ref) {
|
|
230
|
+
this.ref = ref;
|
|
231
|
+
}
|
|
232
|
+
set linkingConfig(linking) {
|
|
233
|
+
this.linking = linking;
|
|
234
|
+
}
|
|
235
|
+
get navRef() {
|
|
236
|
+
return this.ref;
|
|
237
|
+
}
|
|
238
|
+
brandPath(value, route) {
|
|
239
|
+
const boxed = new String(value);
|
|
240
|
+
this.meta.set(boxed, route);
|
|
241
|
+
return boxed;
|
|
242
|
+
}
|
|
243
|
+
getRouteFromPath(url) {
|
|
244
|
+
return this.meta.get(url);
|
|
245
|
+
}
|
|
246
|
+
handleValidationError(error, ctx) {
|
|
247
|
+
try {
|
|
248
|
+
this.onError?.(error, ctx);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// ignore
|
|
252
|
+
}
|
|
253
|
+
if (this.strict) {
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
// non-strict: do not throw
|
|
257
|
+
console.warn(error.message);
|
|
258
|
+
}
|
|
259
|
+
normalizeBuild(route, params) {
|
|
260
|
+
const schema = route.paramsSchema;
|
|
261
|
+
if (!schema)
|
|
262
|
+
return params;
|
|
263
|
+
const res = schema.safeParse(params);
|
|
264
|
+
if (res.success)
|
|
265
|
+
return res.data;
|
|
266
|
+
this.handleValidationError(new Error(`[NavService] build(${route.path}) invalid: ${String(res.error?.message ?? '')}`), { type: 'build', route, input: params });
|
|
267
|
+
return params;
|
|
268
|
+
}
|
|
269
|
+
normalizeQuery(route, query) {
|
|
270
|
+
const schema = route.querySchema;
|
|
271
|
+
if (!schema)
|
|
272
|
+
return query;
|
|
273
|
+
const res = schema.safeParse(query);
|
|
274
|
+
if (res.success)
|
|
275
|
+
return res.data;
|
|
276
|
+
this.handleValidationError(new Error(`[NavService] query(${route.path}) invalid: ${String(res.error?.message ?? '')}`), { type: 'query', route, input: query });
|
|
277
|
+
return query;
|
|
278
|
+
}
|
|
279
|
+
// ───────────────── guards ─────────────────
|
|
280
|
+
ensureRefReady() {
|
|
281
|
+
if (!this.ref) {
|
|
282
|
+
console.warn('[NavService] navRef is not set. Call setRef(ref) or pass local navigation.');
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
if (!this.ref?.isReady?.()) {
|
|
286
|
+
console.warn('[NavService] navRef is not ready yet.');
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
ensureLinking() {
|
|
292
|
+
if (!this.linking?.config) {
|
|
293
|
+
console.warn('[NavService] linking is not set. Call NavService.linking = linking.');
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
// ───────────────── utils ─────────────────
|
|
299
|
+
last(arr) {
|
|
300
|
+
return arr[arr.length - 1];
|
|
301
|
+
}
|
|
302
|
+
getLeafFromState(state) {
|
|
303
|
+
let s = state;
|
|
304
|
+
const parents = [];
|
|
305
|
+
while (s?.routes?.[s.index ?? 0]?.state) {
|
|
306
|
+
const r = s.routes[s.index ?? 0];
|
|
307
|
+
parents.push(r.name);
|
|
308
|
+
s = r.state;
|
|
309
|
+
}
|
|
310
|
+
const leaf = s.routes[s.index ?? 0];
|
|
311
|
+
return { parents, leaf };
|
|
312
|
+
}
|
|
313
|
+
parsePath(path) {
|
|
314
|
+
if (!this.ensureLinking())
|
|
315
|
+
return;
|
|
316
|
+
const opts = this.linking?.config;
|
|
317
|
+
const state = getStateFromPath(path, opts);
|
|
318
|
+
if (!state)
|
|
319
|
+
return;
|
|
320
|
+
return this.getLeafFromState(state);
|
|
321
|
+
}
|
|
322
|
+
buildNestedNavigate(parents, leaf) {
|
|
323
|
+
const payload = parents.reduceRight((child, parent) => ({
|
|
324
|
+
name: parent,
|
|
325
|
+
params: { screen: child.name, params: child.params },
|
|
326
|
+
}), { name: leaf.name, params: leaf.params });
|
|
327
|
+
return CommonActions.navigate(payload);
|
|
328
|
+
}
|
|
329
|
+
ensureAtParents(parentNames) {
|
|
330
|
+
if (!this.ref || !this.ref.isReady?.() || parentNames.length === 0)
|
|
331
|
+
return;
|
|
332
|
+
let action = undefined;
|
|
333
|
+
for (let i = parentNames.length - 1; i >= 0; i--) {
|
|
334
|
+
action = {
|
|
335
|
+
type: CommonActions.navigate('').type,
|
|
336
|
+
payload: {
|
|
337
|
+
name: parentNames[i],
|
|
338
|
+
params: action ? { screen: action.payload.name, params: action.payload.params } : undefined,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (action)
|
|
343
|
+
this.ref.dispatch(action);
|
|
344
|
+
}
|
|
345
|
+
findNavigatorKeyByName(name) {
|
|
346
|
+
if (!name || !this.ref?.getRootState)
|
|
347
|
+
return;
|
|
348
|
+
const walk = (state) => {
|
|
349
|
+
if (!state)
|
|
350
|
+
return;
|
|
351
|
+
const current = state.routes[state.index ?? 0];
|
|
352
|
+
if (current?.name === name && current?.state?.key)
|
|
353
|
+
return current.state.key;
|
|
354
|
+
for (const r of state.routes) {
|
|
355
|
+
const k = walk(r.state);
|
|
356
|
+
if (k)
|
|
357
|
+
return k;
|
|
358
|
+
}
|
|
359
|
+
return;
|
|
360
|
+
};
|
|
361
|
+
return walk(this.ref.getRootState());
|
|
362
|
+
}
|
|
363
|
+
getStackStateByName(name) {
|
|
364
|
+
if (!name || !this.ref?.getRootState)
|
|
365
|
+
return;
|
|
366
|
+
const walk = (state) => {
|
|
367
|
+
if (!state)
|
|
368
|
+
return;
|
|
369
|
+
const current = state.routes[state.index ?? 0];
|
|
370
|
+
if (current?.name === name && current?.state)
|
|
371
|
+
return current.state;
|
|
372
|
+
for (const r of state.routes) {
|
|
373
|
+
const s = walk(r.state);
|
|
374
|
+
if (s)
|
|
375
|
+
return s;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
return walk(this.ref.getRootState());
|
|
379
|
+
}
|
|
380
|
+
getCurrentStackKey() {
|
|
381
|
+
if (!this.ref?.getRootState)
|
|
382
|
+
return;
|
|
383
|
+
let s = this.ref.getRootState();
|
|
384
|
+
let lastKey = s?.key;
|
|
385
|
+
while (s?.routes?.[s.index ?? 0]?.state) {
|
|
386
|
+
s = s.routes[s.index ?? 0].state;
|
|
387
|
+
if (s?.key)
|
|
388
|
+
lastKey = s.key;
|
|
389
|
+
}
|
|
390
|
+
return lastKey;
|
|
391
|
+
}
|
|
392
|
+
pushOrReplace(kind, path) {
|
|
393
|
+
if (!this.ensureRefReady() || !this.ensureLinking())
|
|
394
|
+
return;
|
|
395
|
+
const parsed = this.parsePath(path);
|
|
396
|
+
if (!parsed)
|
|
397
|
+
return;
|
|
398
|
+
const { parents, leaf } = parsed;
|
|
399
|
+
this.ensureAtParents(parents);
|
|
400
|
+
const parent = this.last(parents);
|
|
401
|
+
if (!parent)
|
|
402
|
+
return;
|
|
403
|
+
const targetKey = this.findNavigatorKeyByName(parent);
|
|
404
|
+
if (!targetKey) {
|
|
405
|
+
this.ref?.dispatch(this.buildNestedNavigate(parents, leaf));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const action = kind === 'push' ? StackActions.push(leaf.name, leaf.params) : StackActions.replace(leaf.name, leaf.params);
|
|
409
|
+
this.ref?.dispatch({ ...action, target: targetKey });
|
|
410
|
+
}
|
|
411
|
+
// ───────────── helpers for popTo ─────────────
|
|
412
|
+
hasRoute(state, name) {
|
|
413
|
+
const routes = state?.routes ?? [];
|
|
414
|
+
return routes.some(r => r.name === name);
|
|
415
|
+
}
|
|
416
|
+
routeTreeHasLeaf(route, leafName) {
|
|
417
|
+
let s = route?.state;
|
|
418
|
+
while (s?.routes) {
|
|
419
|
+
const cur = s.routes[s.index ?? 0];
|
|
420
|
+
if (!cur)
|
|
421
|
+
break;
|
|
422
|
+
if (cur.name === leafName)
|
|
423
|
+
return true;
|
|
424
|
+
s = cur.state;
|
|
425
|
+
}
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
buildParamsFromChain(chainBelow, leaf) {
|
|
429
|
+
return chainBelow.reduceRight((child, parent) => ({ screen: parent, params: child }), leaf.params ? { screen: leaf.name, params: leaf.params } : { screen: leaf.name });
|
|
430
|
+
}
|
|
431
|
+
build(route, params) {
|
|
432
|
+
const normalized = params == null ? undefined : this.normalizeBuild(route, params);
|
|
433
|
+
const template = route.path;
|
|
434
|
+
if (normalized == null)
|
|
435
|
+
return this.brandPath(template, route);
|
|
436
|
+
const out = Object.entries(normalized).reduce((acc, [k, v]) => acc.replace(`:${k}`, encodeURIComponent(String(v))), template);
|
|
437
|
+
return this.brandPath(out, route);
|
|
438
|
+
}
|
|
439
|
+
safeJsonStringify(value) {
|
|
440
|
+
try {
|
|
441
|
+
return JSON.stringify(value);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
query(url, q, opts) {
|
|
448
|
+
if (q == null)
|
|
449
|
+
return url;
|
|
450
|
+
const route = this.getRouteFromPath(url);
|
|
451
|
+
const normalized = route ? this.normalizeQuery(route, q) : q;
|
|
452
|
+
const encode = opts?.encode ?? true;
|
|
453
|
+
const nested = opts?.nested ?? 'json';
|
|
454
|
+
const debug = opts?.debug ?? false;
|
|
455
|
+
const nullable = opts?.nullable ?? false;
|
|
456
|
+
const clean = (value) => {
|
|
457
|
+
if (value === undefined)
|
|
458
|
+
return undefined;
|
|
459
|
+
if (value === null)
|
|
460
|
+
return nullable ? null : undefined;
|
|
461
|
+
if (typeof value === 'string' && value.length === 0)
|
|
462
|
+
return undefined;
|
|
463
|
+
if (Array.isArray(value)) {
|
|
464
|
+
const arr = value.map(clean).filter(v => v !== undefined);
|
|
465
|
+
return arr.length ? arr : undefined;
|
|
466
|
+
}
|
|
467
|
+
if (typeof value === 'object') {
|
|
468
|
+
const obj = {};
|
|
469
|
+
for (const [k, v] of Object.entries(value)) {
|
|
470
|
+
const cleaned = clean(v);
|
|
471
|
+
if (cleaned !== undefined)
|
|
472
|
+
obj[k] = cleaned;
|
|
473
|
+
}
|
|
474
|
+
return Object.keys(obj).length ? obj : undefined;
|
|
475
|
+
}
|
|
476
|
+
return value;
|
|
477
|
+
};
|
|
478
|
+
const [beforeHash, hashRest] = url.split('#', 2);
|
|
479
|
+
const hash = hashRest !== undefined ? `#${hashRest}` : '';
|
|
480
|
+
const [base, searchRest] = beforeHash?.split('?', 2) ?? [];
|
|
481
|
+
const existingQuery = (searchRest ? Object.fromEntries(new URLSearchParams(searchRest)) : {});
|
|
482
|
+
const merged = { ...existingQuery, ...normalized };
|
|
483
|
+
const cleaned = clean(merged) ?? {};
|
|
484
|
+
if (debug) {
|
|
485
|
+
console.groupCollapsed('Navigation.query[debug]');
|
|
486
|
+
console.log('base:', base);
|
|
487
|
+
console.log('existing query:', existingQuery);
|
|
488
|
+
console.log('input query:', normalized);
|
|
489
|
+
console.log('merged query:', merged);
|
|
490
|
+
console.log('cleaned query:', cleaned);
|
|
491
|
+
console.log('nested:', nested, 'encode:', encode);
|
|
492
|
+
console.groupEnd();
|
|
493
|
+
}
|
|
494
|
+
const params = new URLSearchParams();
|
|
495
|
+
const pushParam = (key, value) => {
|
|
496
|
+
if (value === undefined)
|
|
497
|
+
return;
|
|
498
|
+
// arrays
|
|
499
|
+
if (Array.isArray(value)) {
|
|
500
|
+
for (const item of value)
|
|
501
|
+
pushParam(key, item);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
// null (optional)
|
|
505
|
+
if (value === null) {
|
|
506
|
+
params.append(key, 'null');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
// plain objects → JSON
|
|
510
|
+
if (typeof value === 'object' && value !== null) {
|
|
511
|
+
const json = this.safeJsonStringify(value);
|
|
512
|
+
if (json !== undefined)
|
|
513
|
+
params.append(key, json);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
// primitives only
|
|
517
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
518
|
+
params.append(key, String(value));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
for (const [k, v] of Object.entries(cleaned)) {
|
|
523
|
+
pushParam(k, v);
|
|
524
|
+
}
|
|
525
|
+
let queryString = params.toString();
|
|
526
|
+
if (!encode && queryString) {
|
|
527
|
+
queryString = decodeURIComponent(queryString);
|
|
528
|
+
}
|
|
529
|
+
const out = queryString ? `${base}?${queryString}${hash}` : `${base}${hash}`;
|
|
530
|
+
if (debug) {
|
|
531
|
+
console.log('[Navigation.query][debug] out:', out);
|
|
532
|
+
}
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
export default new NavService();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { getPathFromState, getStateFromPath, NavigationHelpers, NavigationProp, NavigationState, ParamListBase } from '@react-navigation/native';
|
|
2
|
+
export type GetStateOpts = Parameters<typeof getStateFromPath>[1];
|
|
3
|
+
export type GetPathOpts = Parameters<typeof getPathFromState>[1];
|
|
4
|
+
export type NavLike = NavigationProp<ParamListBase> | NavigationHelpers<ParamListBase> | {
|
|
5
|
+
dispatch: (action: any) => void;
|
|
6
|
+
getState?: () => NavigationState;
|
|
7
|
+
getParent?: () => any;
|
|
8
|
+
popTo?: (name: string, params?: any) => void;
|
|
9
|
+
};
|
|
10
|
+
export type Leaf = {
|
|
11
|
+
name: string;
|
|
12
|
+
params?: Record<string, any>;
|
|
13
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { NavigationValidator, RouteDef } from '../../config/navigation.types';
|
|
2
|
+
export type SafeParseSchema<T = unknown> = {
|
|
3
|
+
safeParse: (input: unknown) => {
|
|
4
|
+
success: true;
|
|
5
|
+
data: T;
|
|
6
|
+
} | {
|
|
7
|
+
success: false;
|
|
8
|
+
error: {
|
|
9
|
+
issues?: any[];
|
|
10
|
+
message?: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export type NavSchemaRegistry = {
|
|
15
|
+
params?: SafeParseSchema;
|
|
16
|
+
query?: SafeParseSchema;
|
|
17
|
+
};
|
|
18
|
+
export type NavSchemas = Record<string, NavSchemaRegistry>;
|
|
19
|
+
declare function createZodValidator(schemas: NavSchemas, opts?: {
|
|
20
|
+
keyOfRoute?: (r: RouteDef<any, any>) => string;
|
|
21
|
+
}): NavigationValidator;
|
|
22
|
+
export default createZodValidator;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function issueMessage(err) {
|
|
2
|
+
const issues = err?.issues;
|
|
3
|
+
if (Array.isArray(issues) && issues.length) {
|
|
4
|
+
const first = issues[0];
|
|
5
|
+
const path = Array.isArray(first?.path) ? first.path.join('.') : '';
|
|
6
|
+
const msg = first?.message ?? 'Invalid';
|
|
7
|
+
return path ? `${path}: ${msg}` : msg;
|
|
8
|
+
}
|
|
9
|
+
return err?.message ?? 'Invalid';
|
|
10
|
+
}
|
|
11
|
+
function createZodValidator(schemas, opts) {
|
|
12
|
+
const keyOfRoute = opts?.keyOfRoute ?? (r => r.linking);
|
|
13
|
+
return {
|
|
14
|
+
validateBuild: (route, params) => {
|
|
15
|
+
const entry = schemas[keyOfRoute(route)];
|
|
16
|
+
const schema = entry?.params;
|
|
17
|
+
if (!schema)
|
|
18
|
+
return { ok: true };
|
|
19
|
+
const res = schema.safeParse(params);
|
|
20
|
+
if (res.success)
|
|
21
|
+
return { ok: true };
|
|
22
|
+
return { ok: false, message: `params invalid: ${issueMessage(res.error)}` };
|
|
23
|
+
},
|
|
24
|
+
validateQuery: (route, query) => {
|
|
25
|
+
const entry = schemas[keyOfRoute(route)];
|
|
26
|
+
const schema = entry?.query;
|
|
27
|
+
if (!schema)
|
|
28
|
+
return { ok: true };
|
|
29
|
+
const res = schema.safeParse(query);
|
|
30
|
+
if (res.success)
|
|
31
|
+
return { ok: true };
|
|
32
|
+
return { ok: false, message: `query invalid: ${issueMessage(res.error)}` };
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export default createZodValidator;
|