@teardown/navigation 2.0.80 → 2.0.82
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/package.json +2 -2
- package/src/hooks/index.ts +14 -0
- package/src/hooks/scoped-hooks.ts +125 -0
- package/src/index.ts +35 -1
- package/src/primitives/define-layout.ts +179 -5
- package/src/primitives/define-screen.ts +144 -5
- package/src/primitives/index.ts +6 -0
- package/src/route-builder/create-root-layout.ts +42 -0
- package/src/route-builder/create-route-tree.ts +128 -0
- package/src/route-builder/index.ts +7 -0
- package/src/route-builder/route-builder-d.test.ts +100 -0
- package/src/route-builder/route-builder.test.ts +276 -0
- package/src/router/create-router.tsx +134 -0
- package/src/router/index.ts +7 -2
- package/src/types/index.ts +14 -0
- package/src/types/route-builder-types.ts +103 -0
- package/src/types/type-utils.ts +37 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teardown/navigation",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.82",
|
|
4
4
|
"description": "Type-safe file-based navigation for React Native",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@biomejs/biome": "2.3.11",
|
|
75
|
-
"@teardown/tsconfig": "2.0.
|
|
75
|
+
"@teardown/tsconfig": "2.0.82",
|
|
76
76
|
"@types/react": "19.2.7",
|
|
77
77
|
"typescript": "5.9.3"
|
|
78
78
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
* Hooks for @teardown/navigation
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
// Scoped hooks (for route-builder pattern)
|
|
6
|
+
export {
|
|
7
|
+
areScopedHooksInitialized,
|
|
8
|
+
clearRouteContext,
|
|
9
|
+
createScopedUseContext,
|
|
10
|
+
createScopedUseNavigate,
|
|
11
|
+
createScopedUseParams,
|
|
12
|
+
getRouteContext,
|
|
13
|
+
getUseNavigation,
|
|
14
|
+
getUseRoute,
|
|
15
|
+
initializeScopedHooks,
|
|
16
|
+
resetScopedHooks,
|
|
17
|
+
setRouteContext,
|
|
18
|
+
} from "./scoped-hooks";
|
|
5
19
|
// Navigation hooks
|
|
6
20
|
export {
|
|
7
21
|
createTypedNavigation,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoped hook factories for @teardown/navigation
|
|
3
|
+
* Creates hooks that capture route context at definition time
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TypedNavigation } from "./use-typed-navigation";
|
|
7
|
+
|
|
8
|
+
// React Native global for dev mode check
|
|
9
|
+
declare const __DEV__: boolean;
|
|
10
|
+
|
|
11
|
+
// Will be injected by createTeardownRouter
|
|
12
|
+
let _useRoute: (() => { params?: object; name?: string }) | null = null;
|
|
13
|
+
let _useNavigation: (() => unknown) | null = null;
|
|
14
|
+
const _routeContextMap: Map<string, unknown> = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize scoped hooks with React Navigation hooks
|
|
18
|
+
* Called by createTeardownRouter during setup
|
|
19
|
+
*/
|
|
20
|
+
export function initializeScopedHooks(
|
|
21
|
+
useRoute: () => { params?: object; name?: string },
|
|
22
|
+
useNavigation: () => unknown
|
|
23
|
+
): void {
|
|
24
|
+
_useRoute = useRoute;
|
|
25
|
+
_useNavigation = useNavigation;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reset scoped hooks (for testing)
|
|
30
|
+
*/
|
|
31
|
+
export function resetScopedHooks(): void {
|
|
32
|
+
_useRoute = null;
|
|
33
|
+
_useNavigation = null;
|
|
34
|
+
_routeContextMap.clear();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set route context (called by loader/beforeLoad)
|
|
39
|
+
*/
|
|
40
|
+
export function setRouteContext(fullPath: string, context: unknown): void {
|
|
41
|
+
_routeContextMap.set(fullPath, context);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get route context
|
|
46
|
+
*/
|
|
47
|
+
export function getRouteContext<T>(fullPath: string): T {
|
|
48
|
+
return _routeContextMap.get(fullPath) as T;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Clear route context (called when navigating away)
|
|
53
|
+
*/
|
|
54
|
+
export function clearRouteContext(fullPath: string): void {
|
|
55
|
+
_routeContextMap.delete(fullPath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if scoped hooks are initialized
|
|
60
|
+
*/
|
|
61
|
+
export function areScopedHooksInitialized(): boolean {
|
|
62
|
+
return _useRoute !== null && _useNavigation !== null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates a scoped useParams hook that captures fullPath at definition time
|
|
67
|
+
* Returns params typed for this specific route
|
|
68
|
+
*/
|
|
69
|
+
export function createScopedUseParams<TParams>(fullPath: string): () => TParams {
|
|
70
|
+
return function useScopedParams(): TParams {
|
|
71
|
+
if (!_useRoute) {
|
|
72
|
+
throw new Error("Scoped hooks not initialized. Wrap your app in TeardownRouter or call initializeScopedHooks.");
|
|
73
|
+
}
|
|
74
|
+
const route = _useRoute();
|
|
75
|
+
|
|
76
|
+
// DEV mode: validate we're on the correct route
|
|
77
|
+
if (__DEV__ && route.name) {
|
|
78
|
+
// Could add route name validation here if needed
|
|
79
|
+
// For now, we trust the type system
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (route.params ?? {}) as TParams;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates a scoped useRouteContext hook
|
|
88
|
+
* Returns context set by this route's loader/beforeLoad
|
|
89
|
+
*/
|
|
90
|
+
export function createScopedUseContext<TContext>(fullPath: string): () => TContext {
|
|
91
|
+
return function useScopedContext(): TContext {
|
|
92
|
+
return getRouteContext<TContext>(fullPath);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates a scoped useNavigate hook
|
|
98
|
+
* Returns typed navigation relative to this route
|
|
99
|
+
*/
|
|
100
|
+
export function createScopedUseNavigate(fullPath: string): () => TypedNavigation {
|
|
101
|
+
return function useScopedNavigate(): TypedNavigation {
|
|
102
|
+
if (!_useNavigation) {
|
|
103
|
+
throw new Error("Scoped hooks not initialized. Wrap your app in TeardownRouter or call initializeScopedHooks.");
|
|
104
|
+
}
|
|
105
|
+
const navigation = _useNavigation();
|
|
106
|
+
// Import dynamically to avoid circular deps
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
108
|
+
const { createTypedNavigation } = require("./use-typed-navigation");
|
|
109
|
+
return createTypedNavigation(navigation, fullPath);
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get the internal useRoute hook (for advanced usage)
|
|
115
|
+
*/
|
|
116
|
+
export function getUseRoute(): (() => { params?: object; name?: string }) | null {
|
|
117
|
+
return _useRoute;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get the internal useNavigation hook (for advanced usage)
|
|
122
|
+
*/
|
|
123
|
+
export function getUseNavigation(): (() => unknown) | null {
|
|
124
|
+
return _useNavigation;
|
|
125
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,11 +6,16 @@
|
|
|
6
6
|
export type { TypedNavigation, TypedRoute } from "./hooks";
|
|
7
7
|
// Hooks
|
|
8
8
|
export {
|
|
9
|
+
areScopedHooksInitialized,
|
|
10
|
+
createScopedUseContext,
|
|
11
|
+
createScopedUseNavigate,
|
|
12
|
+
createScopedUseParams,
|
|
9
13
|
createTypedNavigation,
|
|
10
14
|
createTypedRoute,
|
|
11
15
|
createUseTypedNavigation,
|
|
12
16
|
createUseTypedParams,
|
|
13
17
|
createUseTypedRoute,
|
|
18
|
+
initializeScopedHooks,
|
|
14
19
|
useTypedNavigation,
|
|
15
20
|
useTypedParams,
|
|
16
21
|
useTypedRoute,
|
|
@@ -19,6 +24,10 @@ export type {
|
|
|
19
24
|
BaseScreenOptions,
|
|
20
25
|
DrawerLayoutConfig,
|
|
21
26
|
DrawerScreenOptions,
|
|
27
|
+
EnhancedLayoutConfig,
|
|
28
|
+
EnhancedLayoutDefinition,
|
|
29
|
+
EnhancedScreenConfig,
|
|
30
|
+
EnhancedScreenDefinition,
|
|
22
31
|
InferParamSchemaOutput,
|
|
23
32
|
LayoutConfig,
|
|
24
33
|
LayoutDefinition,
|
|
@@ -38,10 +47,15 @@ export {
|
|
|
38
47
|
createParamSchema,
|
|
39
48
|
defineLayout,
|
|
40
49
|
defineScreen,
|
|
50
|
+
isEnhancedLayoutDefinition,
|
|
51
|
+
isEnhancedScreenDefinition,
|
|
41
52
|
isLayoutDefinition,
|
|
42
53
|
isScreenDefinition,
|
|
43
54
|
paramValidators,
|
|
44
55
|
} from "./primitives";
|
|
56
|
+
export type { CreateRouteTreeConfig, RootLayoutConfig, RouteTree as BuilderRouteTree } from "./route-builder";
|
|
57
|
+
// Route Builder
|
|
58
|
+
export { createRootLayout, createRouteTree, flattenRouteTree } from "./route-builder";
|
|
45
59
|
// Router
|
|
46
60
|
export type {
|
|
47
61
|
ExtractRoutePaths,
|
|
@@ -51,10 +65,28 @@ export type {
|
|
|
51
65
|
NavigatorNode,
|
|
52
66
|
RouteTree,
|
|
53
67
|
RouteTreeEntry,
|
|
68
|
+
RouteTreeRouterOptions,
|
|
54
69
|
ScreenEntry,
|
|
55
70
|
TeardownRouterOptions,
|
|
56
71
|
} from "./router";
|
|
57
|
-
export {
|
|
72
|
+
export {
|
|
73
|
+
createTeardownRouter,
|
|
74
|
+
createTeardownRouterFromTree,
|
|
75
|
+
isFlatRouteTree,
|
|
76
|
+
isHierarchicalRouteTree,
|
|
77
|
+
} from "./router";
|
|
78
|
+
// Route builder types
|
|
79
|
+
export type {
|
|
80
|
+
AccumulateParams,
|
|
81
|
+
AnyRouteDefinition,
|
|
82
|
+
ComputeFullPath,
|
|
83
|
+
ExtractRouteParams,
|
|
84
|
+
ExtractRoutePaths as ExtractRoutePathsFromTree,
|
|
85
|
+
MergeRouteParams,
|
|
86
|
+
ParamsForPath,
|
|
87
|
+
RouteDefinitionBase,
|
|
88
|
+
StripRouteGroups,
|
|
89
|
+
} from "./types/route-builder-types";
|
|
58
90
|
// Types
|
|
59
91
|
export type {
|
|
60
92
|
NavigationState,
|
|
@@ -74,9 +106,11 @@ export type {
|
|
|
74
106
|
InferParams,
|
|
75
107
|
IsCatchAllSegment,
|
|
76
108
|
IsDynamicSegment,
|
|
109
|
+
IsEmptyParams,
|
|
77
110
|
IsOptionalSegment,
|
|
78
111
|
NavigateArgs,
|
|
79
112
|
Simplify,
|
|
113
|
+
ToReactNavigationPath,
|
|
80
114
|
} from "./types/type-utils";
|
|
81
115
|
export type { MatchResult, ParsedPath } from "./utils";
|
|
82
116
|
// Utils
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ComponentType } from "react";
|
|
7
|
+
import { createScopedUseContext, createScopedUseNavigate, createScopedUseParams } from "../hooks/scoped-hooks";
|
|
8
|
+
import type { TypedNavigation } from "../hooks/use-typed-navigation";
|
|
9
|
+
import type {
|
|
10
|
+
AccumulateParams,
|
|
11
|
+
AnyRouteDefinition,
|
|
12
|
+
ComputeFullPath,
|
|
13
|
+
RouteDefinitionBase,
|
|
14
|
+
} from "../types/route-builder-types";
|
|
15
|
+
import type { InferParams } from "../types/type-utils";
|
|
7
16
|
|
|
8
17
|
/**
|
|
9
18
|
* Navigator types supported by the library
|
|
@@ -135,14 +144,86 @@ export interface DrawerLayoutConfig extends BaseLayoutConfig {
|
|
|
135
144
|
export type LayoutConfig = StackLayoutConfig | TabsLayoutConfig | DrawerLayoutConfig;
|
|
136
145
|
|
|
137
146
|
/**
|
|
138
|
-
* Brand type for layout definitions
|
|
147
|
+
* Brand type for layout definitions (legacy)
|
|
139
148
|
*/
|
|
140
149
|
export type LayoutDefinition<T extends LayoutConfig = LayoutConfig> = T & {
|
|
141
150
|
__brand: "TeardownLayout";
|
|
142
151
|
};
|
|
143
152
|
|
|
144
153
|
/**
|
|
145
|
-
*
|
|
154
|
+
* Enhanced layout configuration with path and parent
|
|
155
|
+
*/
|
|
156
|
+
export interface EnhancedLayoutConfig<
|
|
157
|
+
TType extends NavigatorType,
|
|
158
|
+
TPath extends string,
|
|
159
|
+
TParent extends RouteDefinitionBase | undefined,
|
|
160
|
+
TContext,
|
|
161
|
+
> {
|
|
162
|
+
/** Navigator type */
|
|
163
|
+
type: TType;
|
|
164
|
+
/** This route segment */
|
|
165
|
+
path: TPath;
|
|
166
|
+
/** Links to parent route */
|
|
167
|
+
getParentRoute?: () => TParent;
|
|
168
|
+
/** Default screen options for children */
|
|
169
|
+
screenOptions?: object;
|
|
170
|
+
/** Initial route name */
|
|
171
|
+
initialRouteName?: string;
|
|
172
|
+
/** Async loader */
|
|
173
|
+
loader?: () => TContext | Promise<TContext>;
|
|
174
|
+
/** Custom tab bar (tabs only) */
|
|
175
|
+
tabBar?: ComponentType;
|
|
176
|
+
/** Custom drawer content (drawer only) */
|
|
177
|
+
drawerContent?: ComponentType;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Enhanced layout definition with addChildren
|
|
182
|
+
*/
|
|
183
|
+
export interface EnhancedLayoutDefinition<
|
|
184
|
+
TType extends NavigatorType,
|
|
185
|
+
TPath extends string,
|
|
186
|
+
TFullPath extends string,
|
|
187
|
+
TParams,
|
|
188
|
+
TAllParams,
|
|
189
|
+
TContext,
|
|
190
|
+
TChildren extends readonly AnyRouteDefinition[] = readonly [],
|
|
191
|
+
> extends RouteDefinitionBase<TPath, TFullPath, TParams, TAllParams, TContext> {
|
|
192
|
+
__brand: "TeardownLayout";
|
|
193
|
+
type: TType;
|
|
194
|
+
screenOptions?: object;
|
|
195
|
+
initialRouteName?: string;
|
|
196
|
+
loader?: () => TContext | Promise<TContext>;
|
|
197
|
+
children: TChildren;
|
|
198
|
+
|
|
199
|
+
/** Add children routes - returns new layout with updated type */
|
|
200
|
+
addChildren<TNewChildren extends readonly AnyRouteDefinition[]>(
|
|
201
|
+
children: TNewChildren
|
|
202
|
+
): EnhancedLayoutDefinition<TType, TPath, TFullPath, TParams, TAllParams, TContext, TNewChildren>;
|
|
203
|
+
|
|
204
|
+
/** Scoped hooks */
|
|
205
|
+
useParams: () => TAllParams;
|
|
206
|
+
useRouteContext: () => TContext;
|
|
207
|
+
useNavigate: () => TypedNavigation;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Runtime full path computation
|
|
212
|
+
*/
|
|
213
|
+
function computeFullPathRuntime<TPath extends string, TParent>(path: TPath, parent?: TParent): string {
|
|
214
|
+
const cleanPath = path.replace(/\(([^)]+)\)\/?/g, "");
|
|
215
|
+
|
|
216
|
+
if (!parent || !(parent as Record<string, unknown>).fullPath) {
|
|
217
|
+
return cleanPath ? `/${cleanPath}` : "/";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const parentPath = (parent as Record<string, unknown>).fullPath as string;
|
|
221
|
+
if (!cleanPath) return parentPath;
|
|
222
|
+
return parentPath === "/" ? `/${cleanPath}` : `${parentPath}/${cleanPath}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Defines a layout with type-safe configuration (legacy API)
|
|
146
227
|
*
|
|
147
228
|
* @example
|
|
148
229
|
* ```tsx
|
|
@@ -176,11 +257,90 @@ export type LayoutDefinition<T extends LayoutConfig = LayoutConfig> = T & {
|
|
|
176
257
|
* });
|
|
177
258
|
* ```
|
|
178
259
|
*/
|
|
179
|
-
export function defineLayout<T extends LayoutConfig>(config: T): LayoutDefinition<T
|
|
260
|
+
export function defineLayout<T extends LayoutConfig>(config: T): LayoutDefinition<T>;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Defines a layout with enhanced type-safe configuration
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```tsx
|
|
267
|
+
* const usersLayout = defineLayout({
|
|
268
|
+
* type: 'stack',
|
|
269
|
+
* path: 'users',
|
|
270
|
+
* getParentRoute: () => rootLayout,
|
|
271
|
+
* });
|
|
272
|
+
*
|
|
273
|
+
* // Use addChildren to add screens
|
|
274
|
+
* const routeTree = usersLayout.addChildren([userProfile, userSettings]);
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
export function defineLayout<
|
|
278
|
+
TType extends NavigatorType,
|
|
279
|
+
TPath extends string,
|
|
280
|
+
TParent extends RouteDefinitionBase | undefined = undefined,
|
|
281
|
+
TContext = unknown,
|
|
282
|
+
>(
|
|
283
|
+
config: EnhancedLayoutConfig<TType, TPath, TParent, TContext>
|
|
284
|
+
): EnhancedLayoutDefinition<
|
|
285
|
+
TType,
|
|
286
|
+
TPath,
|
|
287
|
+
ComputeFullPath<TPath, TParent>,
|
|
288
|
+
InferParams<TPath>,
|
|
289
|
+
AccumulateParams<TPath, TParent>,
|
|
290
|
+
TContext
|
|
291
|
+
>;
|
|
292
|
+
|
|
293
|
+
// Implementation
|
|
294
|
+
export function defineLayout<
|
|
295
|
+
T extends LayoutConfig = LayoutConfig,
|
|
296
|
+
TType extends NavigatorType = NavigatorType,
|
|
297
|
+
TPath extends string = string,
|
|
298
|
+
TParent extends RouteDefinitionBase | undefined = undefined,
|
|
299
|
+
TContext = unknown,
|
|
300
|
+
>(
|
|
301
|
+
config: T | EnhancedLayoutConfig<TType, TPath, TParent, TContext>
|
|
302
|
+
): LayoutDefinition<T> | EnhancedLayoutDefinition<TType, TPath, string, unknown, unknown, TContext> {
|
|
303
|
+
// Check if this is an enhanced config (has 'path' property)
|
|
304
|
+
if ("path" in config && typeof config.path === "string") {
|
|
305
|
+
const enhancedConfig = config as EnhancedLayoutConfig<TType, TPath, TParent, TContext>;
|
|
306
|
+
const parent = enhancedConfig.getParentRoute?.();
|
|
307
|
+
const fullPath = computeFullPathRuntime(enhancedConfig.path, parent);
|
|
308
|
+
|
|
309
|
+
const layout: EnhancedLayoutDefinition<TType, TPath, string, unknown, unknown, TContext> = {
|
|
310
|
+
__brand: "TeardownLayout" as const,
|
|
311
|
+
type: enhancedConfig.type,
|
|
312
|
+
path: enhancedConfig.path,
|
|
313
|
+
fullPath: fullPath,
|
|
314
|
+
params: {} as InferParams<TPath>,
|
|
315
|
+
allParams: {} as AccumulateParams<TPath, TParent>,
|
|
316
|
+
context: {} as TContext,
|
|
317
|
+
screenOptions: enhancedConfig.screenOptions,
|
|
318
|
+
initialRouteName: enhancedConfig.initialRouteName,
|
|
319
|
+
loader: enhancedConfig.loader,
|
|
320
|
+
children: [] as const,
|
|
321
|
+
|
|
322
|
+
// addChildren returns new layout with children attached
|
|
323
|
+
addChildren(children) {
|
|
324
|
+
return {
|
|
325
|
+
...this,
|
|
326
|
+
children,
|
|
327
|
+
} as EnhancedLayoutDefinition<TType, TPath, string, unknown, unknown, TContext, typeof children>;
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
// Scoped hooks
|
|
331
|
+
useParams: createScopedUseParams(fullPath),
|
|
332
|
+
useRouteContext: createScopedUseContext(fullPath),
|
|
333
|
+
useNavigate: createScopedUseNavigate(fullPath),
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
return layout;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Legacy API
|
|
180
340
|
return {
|
|
181
|
-
...config,
|
|
341
|
+
...(config as T),
|
|
182
342
|
__brand: "TeardownLayout" as const,
|
|
183
|
-
}
|
|
343
|
+
} as LayoutDefinition<T>;
|
|
184
344
|
}
|
|
185
345
|
|
|
186
346
|
/**
|
|
@@ -189,3 +349,17 @@ export function defineLayout<T extends LayoutConfig>(config: T): LayoutDefinitio
|
|
|
189
349
|
export function isLayoutDefinition(value: unknown): value is LayoutDefinition {
|
|
190
350
|
return typeof value === "object" && value !== null && (value as LayoutDefinition).__brand === "TeardownLayout";
|
|
191
351
|
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Type guard to check if a value is an enhanced layout definition
|
|
355
|
+
*/
|
|
356
|
+
export function isEnhancedLayoutDefinition(
|
|
357
|
+
value: unknown
|
|
358
|
+
): value is EnhancedLayoutDefinition<NavigatorType, string, string, unknown, unknown, unknown> {
|
|
359
|
+
return (
|
|
360
|
+
isLayoutDefinition(value) &&
|
|
361
|
+
"fullPath" in value &&
|
|
362
|
+
"addChildren" in value &&
|
|
363
|
+
typeof (value as Record<string, unknown>).addChildren === "function"
|
|
364
|
+
);
|
|
365
|
+
}
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ComponentType } from "react";
|
|
7
|
+
import { createScopedUseContext, createScopedUseNavigate, createScopedUseParams } from "../hooks/scoped-hooks";
|
|
8
|
+
import type { TypedNavigation } from "../hooks/use-typed-navigation";
|
|
9
|
+
import type { AccumulateParams, ComputeFullPath, RouteDefinitionBase } from "../types/route-builder-types";
|
|
10
|
+
import type { InferParams } from "../types/type-utils";
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Screen options that can be passed to React Navigation
|
|
@@ -52,7 +56,7 @@ export interface ScreenOptions {
|
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/**
|
|
55
|
-
* Screen configuration
|
|
59
|
+
* Screen configuration (legacy)
|
|
56
60
|
*/
|
|
57
61
|
export interface ScreenConfig<TParams = unknown> {
|
|
58
62
|
/**
|
|
@@ -82,14 +86,69 @@ export interface ScreenConfig<TParams = unknown> {
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
/**
|
|
85
|
-
* Brand type for screen definitions
|
|
89
|
+
* Brand type for screen definitions (legacy)
|
|
86
90
|
*/
|
|
87
91
|
export interface ScreenDefinition<TParams = unknown> extends ScreenConfig<TParams> {
|
|
88
92
|
__brand: "TeardownScreen";
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
/**
|
|
92
|
-
*
|
|
96
|
+
* Enhanced screen configuration with path and parent
|
|
97
|
+
*/
|
|
98
|
+
export interface EnhancedScreenConfig<TPath extends string, TParent extends RouteDefinitionBase | undefined, TContext> {
|
|
99
|
+
/** This route segment (e.g., "[userId]") */
|
|
100
|
+
path: TPath;
|
|
101
|
+
/** Links to parent route for path computation */
|
|
102
|
+
getParentRoute?: () => TParent;
|
|
103
|
+
/** Component to render */
|
|
104
|
+
component: ComponentType;
|
|
105
|
+
/** Navigation options */
|
|
106
|
+
options?: ScreenOptions | ((props: { route: unknown; navigation: unknown }) => ScreenOptions);
|
|
107
|
+
/** Async loader - returns context for this route */
|
|
108
|
+
loader?: () => TContext | Promise<TContext>;
|
|
109
|
+
/** Navigation event listeners */
|
|
110
|
+
listeners?: {
|
|
111
|
+
focus?: () => void;
|
|
112
|
+
blur?: () => void;
|
|
113
|
+
beforeRemove?: (e: { preventDefault: () => void }) => void;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Enhanced screen definition with scoped hooks
|
|
119
|
+
*/
|
|
120
|
+
export interface EnhancedScreenDefinition<TPath extends string, TFullPath extends string, TParams, TAllParams, TContext>
|
|
121
|
+
extends RouteDefinitionBase<TPath, TFullPath, TParams, TAllParams, TContext> {
|
|
122
|
+
__brand: "TeardownScreen";
|
|
123
|
+
component: ComponentType;
|
|
124
|
+
options?: ScreenOptions | ((props: unknown) => ScreenOptions);
|
|
125
|
+
loader?: () => TContext | Promise<TContext>;
|
|
126
|
+
listeners?: object;
|
|
127
|
+
|
|
128
|
+
/** Scoped hooks - auto-infer types! */
|
|
129
|
+
useParams: () => TAllParams;
|
|
130
|
+
useRouteContext: () => TContext;
|
|
131
|
+
useNavigate: () => TypedNavigation;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Runtime full path computation
|
|
136
|
+
*/
|
|
137
|
+
function computeFullPathRuntime<TPath extends string, TParent>(path: TPath, parent?: TParent): string {
|
|
138
|
+
// Strip route groups
|
|
139
|
+
const cleanPath = path.replace(/\(([^)]+)\)\/?/g, "");
|
|
140
|
+
|
|
141
|
+
if (!parent || !(parent as Record<string, unknown>).fullPath) {
|
|
142
|
+
return cleanPath ? `/${cleanPath}` : "/";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const parentPath = (parent as Record<string, unknown>).fullPath as string;
|
|
146
|
+
if (!cleanPath) return parentPath;
|
|
147
|
+
return parentPath === "/" ? `/${cleanPath}` : `${parentPath}/${cleanPath}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Defines a screen with type-safe configuration (legacy API)
|
|
93
152
|
*
|
|
94
153
|
* @example
|
|
95
154
|
* ```tsx
|
|
@@ -108,9 +167,75 @@ export interface ScreenDefinition<TParams = unknown> extends ScreenConfig<TParam
|
|
|
108
167
|
* });
|
|
109
168
|
* ```
|
|
110
169
|
*/
|
|
111
|
-
export function defineScreen<TParams = unknown>(config: ScreenConfig<TParams>): ScreenDefinition<TParams
|
|
170
|
+
export function defineScreen<TParams = unknown>(config: ScreenConfig<TParams>): ScreenDefinition<TParams>;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Defines a screen with enhanced type-safe configuration
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```tsx
|
|
177
|
+
* const userProfile = defineScreen({
|
|
178
|
+
* path: '[userId]',
|
|
179
|
+
* getParentRoute: () => usersLayout,
|
|
180
|
+
* component: UserProfile,
|
|
181
|
+
* });
|
|
182
|
+
*
|
|
183
|
+
* // In component:
|
|
184
|
+
* const { userId } = userProfile.useParams(); // Typed!
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export function defineScreen<
|
|
188
|
+
TPath extends string,
|
|
189
|
+
TParent extends RouteDefinitionBase | undefined = undefined,
|
|
190
|
+
TContext = unknown,
|
|
191
|
+
>(
|
|
192
|
+
config: EnhancedScreenConfig<TPath, TParent, TContext>
|
|
193
|
+
): EnhancedScreenDefinition<
|
|
194
|
+
TPath,
|
|
195
|
+
ComputeFullPath<TPath, TParent>,
|
|
196
|
+
InferParams<TPath>,
|
|
197
|
+
AccumulateParams<TPath, TParent>,
|
|
198
|
+
TContext
|
|
199
|
+
>;
|
|
200
|
+
|
|
201
|
+
// Implementation
|
|
202
|
+
export function defineScreen<
|
|
203
|
+
TPath extends string = string,
|
|
204
|
+
TParent extends RouteDefinitionBase | undefined = undefined,
|
|
205
|
+
TContext = unknown,
|
|
206
|
+
TParams = unknown,
|
|
207
|
+
>(
|
|
208
|
+
config: ScreenConfig<TParams> | EnhancedScreenConfig<TPath, TParent, TContext>
|
|
209
|
+
): ScreenDefinition<TParams> | EnhancedScreenDefinition<TPath, string, unknown, unknown, TContext> {
|
|
210
|
+
// Check if this is an enhanced config (has 'path' property)
|
|
211
|
+
if ("path" in config && typeof config.path === "string") {
|
|
212
|
+
const enhancedConfig = config as EnhancedScreenConfig<TPath, TParent, TContext>;
|
|
213
|
+
const parent = enhancedConfig.getParentRoute?.();
|
|
214
|
+
const fullPath = computeFullPathRuntime(enhancedConfig.path, parent);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
__brand: "TeardownScreen" as const,
|
|
218
|
+
path: enhancedConfig.path,
|
|
219
|
+
fullPath: fullPath as ComputeFullPath<TPath, TParent>,
|
|
220
|
+
params: {} as InferParams<TPath>,
|
|
221
|
+
allParams: {} as AccumulateParams<TPath, TParent>,
|
|
222
|
+
context: {} as TContext,
|
|
223
|
+
component: enhancedConfig.component,
|
|
224
|
+
options: enhancedConfig.options,
|
|
225
|
+
loader: enhancedConfig.loader,
|
|
226
|
+
listeners: enhancedConfig.listeners,
|
|
227
|
+
|
|
228
|
+
// Scoped hooks
|
|
229
|
+
useParams: createScopedUseParams<AccumulateParams<TPath, TParent>>(fullPath),
|
|
230
|
+
useRouteContext: createScopedUseContext<TContext>(fullPath),
|
|
231
|
+
useNavigate: createScopedUseNavigate(fullPath),
|
|
232
|
+
} as EnhancedScreenDefinition<TPath, string, unknown, unknown, TContext>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Legacy API
|
|
236
|
+
const legacyConfig = config as ScreenConfig<TParams>;
|
|
112
237
|
return {
|
|
113
|
-
...
|
|
238
|
+
...legacyConfig,
|
|
114
239
|
__brand: "TeardownScreen" as const,
|
|
115
240
|
};
|
|
116
241
|
}
|
|
@@ -121,3 +246,17 @@ export function defineScreen<TParams = unknown>(config: ScreenConfig<TParams>):
|
|
|
121
246
|
export function isScreenDefinition(value: unknown): value is ScreenDefinition {
|
|
122
247
|
return typeof value === "object" && value !== null && (value as ScreenDefinition).__brand === "TeardownScreen";
|
|
123
248
|
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Type guard to check if a value is an enhanced screen definition
|
|
252
|
+
*/
|
|
253
|
+
export function isEnhancedScreenDefinition(
|
|
254
|
+
value: unknown
|
|
255
|
+
): value is EnhancedScreenDefinition<string, string, unknown, unknown, unknown> {
|
|
256
|
+
return (
|
|
257
|
+
isScreenDefinition(value) &&
|
|
258
|
+
"fullPath" in value &&
|
|
259
|
+
"useParams" in value &&
|
|
260
|
+
typeof (value as Record<string, unknown>).useParams === "function"
|
|
261
|
+
);
|
|
262
|
+
}
|
package/src/primitives/index.ts
CHANGED
|
@@ -17,6 +17,9 @@ export {
|
|
|
17
17
|
type DrawerLayoutConfig,
|
|
18
18
|
type DrawerScreenOptions,
|
|
19
19
|
defineLayout,
|
|
20
|
+
type EnhancedLayoutConfig,
|
|
21
|
+
type EnhancedLayoutDefinition,
|
|
22
|
+
isEnhancedLayoutDefinition,
|
|
20
23
|
isLayoutDefinition,
|
|
21
24
|
type LayoutConfig,
|
|
22
25
|
type LayoutDefinition,
|
|
@@ -30,6 +33,9 @@ export {
|
|
|
30
33
|
// Screen definitions
|
|
31
34
|
export {
|
|
32
35
|
defineScreen,
|
|
36
|
+
type EnhancedScreenConfig,
|
|
37
|
+
type EnhancedScreenDefinition,
|
|
38
|
+
isEnhancedScreenDefinition,
|
|
33
39
|
isScreenDefinition,
|
|
34
40
|
type ScreenConfig,
|
|
35
41
|
type ScreenDefinition,
|