@teardown/navigation 2.0.70 → 2.0.71
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/index.ts +13 -0
- package/src/router/create-router.tsx +282 -0
- package/src/router/index.ts +19 -0
- package/src/router/types.ts +107 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teardown/navigation",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.71",
|
|
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.71",
|
|
76
76
|
"@types/react": "19.2.7",
|
|
77
77
|
"typescript": "5.9.3"
|
|
78
78
|
}
|
package/src/index.ts
CHANGED
|
@@ -42,6 +42,19 @@ export {
|
|
|
42
42
|
isScreenDefinition,
|
|
43
43
|
paramValidators,
|
|
44
44
|
} from "./primitives";
|
|
45
|
+
// Router
|
|
46
|
+
export type {
|
|
47
|
+
ExtractRoutePaths,
|
|
48
|
+
ExtractScreens,
|
|
49
|
+
FlatRouteTree,
|
|
50
|
+
FlatRouteTreeEntry,
|
|
51
|
+
NavigatorNode,
|
|
52
|
+
RouteTree,
|
|
53
|
+
RouteTreeEntry,
|
|
54
|
+
ScreenEntry,
|
|
55
|
+
TeardownRouterOptions,
|
|
56
|
+
} from "./router";
|
|
57
|
+
export { createTeardownRouter, isFlatRouteTree, isHierarchicalRouteTree } from "./router";
|
|
45
58
|
// Types
|
|
46
59
|
export type {
|
|
47
60
|
NavigationState,
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createTeardownRouter - Factory function for creating a type-safe router
|
|
3
|
+
* Supports nested Stack, Tab, and Drawer navigators
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LinkingOptions, Theme } from "@react-navigation/native";
|
|
7
|
+
import { NavigationContainer } from "@react-navigation/native";
|
|
8
|
+
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
|
9
|
+
import type React from "react";
|
|
10
|
+
import type { ComponentType, ReactNode } from "react";
|
|
11
|
+
import type { FlatRouteTree, NavigatorNode, ScreenEntry } from "./types";
|
|
12
|
+
import { isHierarchicalRouteTree } from "./types";
|
|
13
|
+
|
|
14
|
+
// Lazy imports for optional navigators
|
|
15
|
+
let createBottomTabNavigator: (() => ReturnType<typeof createNativeStackNavigator>) | null = null;
|
|
16
|
+
let createDrawerNavigator: (() => ReturnType<typeof createNativeStackNavigator>) | null = null;
|
|
17
|
+
|
|
18
|
+
// Try to load optional dependencies
|
|
19
|
+
try {
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
|
+
const bottomTabs = require("@react-navigation/bottom-tabs");
|
|
22
|
+
createBottomTabNavigator = bottomTabs.createBottomTabNavigator;
|
|
23
|
+
} catch {
|
|
24
|
+
// Bottom tabs not installed
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
|
+
const drawer = require("@react-navigation/drawer");
|
|
30
|
+
createDrawerNavigator = drawer.createDrawerNavigator;
|
|
31
|
+
} catch {
|
|
32
|
+
// Drawer not installed
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options for creating a Teardown router
|
|
37
|
+
*/
|
|
38
|
+
export interface TeardownRouterOptions<T extends NavigatorNode | FlatRouteTree = NavigatorNode> {
|
|
39
|
+
/**
|
|
40
|
+
* The generated route tree from routeTree.generated.ts
|
|
41
|
+
* Supports both hierarchical (NavigatorNode) and flat (legacy) formats
|
|
42
|
+
*/
|
|
43
|
+
routeTree: T;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Initial route name (screen key without leading slash)
|
|
47
|
+
* @default first route in routeTree
|
|
48
|
+
*/
|
|
49
|
+
initialRouteName?: string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Deep linking configuration
|
|
53
|
+
*/
|
|
54
|
+
linking?: LinkingOptions<Record<string, unknown>>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Navigation theme
|
|
58
|
+
*/
|
|
59
|
+
theme?: Theme;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fallback component shown when navigating to unknown routes
|
|
63
|
+
*/
|
|
64
|
+
notFoundComponent?: ComponentType;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Custom navigation container wrapper
|
|
68
|
+
*/
|
|
69
|
+
navigationContainerProps?: Omit<React.ComponentProps<typeof NavigationContainer>, "children">;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Creates a navigator component based on type
|
|
74
|
+
*/
|
|
75
|
+
function createNavigator(type: "stack" | "tabs" | "drawer") {
|
|
76
|
+
switch (type) {
|
|
77
|
+
case "tabs":
|
|
78
|
+
if (!createBottomTabNavigator) {
|
|
79
|
+
console.warn(
|
|
80
|
+
"[@teardown/navigation] Tab navigator requested but @react-navigation/bottom-tabs is not installed. Falling back to stack."
|
|
81
|
+
);
|
|
82
|
+
return createNativeStackNavigator();
|
|
83
|
+
}
|
|
84
|
+
return createBottomTabNavigator();
|
|
85
|
+
case "drawer":
|
|
86
|
+
if (!createDrawerNavigator) {
|
|
87
|
+
console.warn(
|
|
88
|
+
"[@teardown/navigation] Drawer navigator requested but @react-navigation/drawer is not installed. Falling back to stack."
|
|
89
|
+
);
|
|
90
|
+
return createNativeStackNavigator();
|
|
91
|
+
}
|
|
92
|
+
return createDrawerNavigator();
|
|
93
|
+
default:
|
|
94
|
+
return createNativeStackNavigator();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Gets default screen options based on navigator type
|
|
100
|
+
*/
|
|
101
|
+
function getDefaultScreenOptions(type: "stack" | "tabs" | "drawer") {
|
|
102
|
+
switch (type) {
|
|
103
|
+
case "tabs":
|
|
104
|
+
return {
|
|
105
|
+
headerShown: false,
|
|
106
|
+
};
|
|
107
|
+
case "drawer":
|
|
108
|
+
return {
|
|
109
|
+
headerShown: true,
|
|
110
|
+
};
|
|
111
|
+
default:
|
|
112
|
+
return {
|
|
113
|
+
headerShown: true,
|
|
114
|
+
animation: "slide_from_right" as const,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Renders a navigator node recursively
|
|
121
|
+
*/
|
|
122
|
+
function RenderNavigator({ node }: { node: NavigatorNode }): React.JSX.Element {
|
|
123
|
+
const Navigator = createNavigator(node.type);
|
|
124
|
+
const defaultOptions = getDefaultScreenOptions(node.type);
|
|
125
|
+
|
|
126
|
+
// Get screen options from layout if available
|
|
127
|
+
const layoutOptions = node.layout?.screenOptions || {};
|
|
128
|
+
const initialRouteName = node.layout?.initialRouteName || node.initialRouteName;
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Navigator.Navigator
|
|
132
|
+
initialRouteName={initialRouteName}
|
|
133
|
+
screenOptions={{
|
|
134
|
+
...defaultOptions,
|
|
135
|
+
...layoutOptions,
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
{/* Render screens */}
|
|
139
|
+
{Object.entries(node.screens).map(([name, entry]) => {
|
|
140
|
+
const screenEntry = entry as ScreenEntry;
|
|
141
|
+
const screenDef = screenEntry.screen;
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<Navigator.Screen
|
|
145
|
+
key={name}
|
|
146
|
+
name={name}
|
|
147
|
+
component={screenDef.component}
|
|
148
|
+
options={typeof screenDef.options === "function" ? screenDef.options : screenDef.options}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
})}
|
|
152
|
+
|
|
153
|
+
{/* Render nested navigators as screens */}
|
|
154
|
+
{node.navigators &&
|
|
155
|
+
Object.entries(node.navigators).map(([name, nestedNode]) => {
|
|
156
|
+
// Create a component that renders the nested navigator
|
|
157
|
+
const NestedNavigatorComponent = () => <RenderNavigator node={nestedNode} />;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<Navigator.Screen
|
|
161
|
+
key={name}
|
|
162
|
+
name={name}
|
|
163
|
+
component={NestedNavigatorComponent}
|
|
164
|
+
options={{ headerShown: false }}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
</Navigator.Navigator>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Creates a Teardown Router component from a generated route tree
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```tsx
|
|
177
|
+
* import { createTeardownRouter } from "@teardown/navigation";
|
|
178
|
+
* import { routeTree } from "../.teardown/routeTree.generated";
|
|
179
|
+
*
|
|
180
|
+
* export const Router = createTeardownRouter({
|
|
181
|
+
* routeTree,
|
|
182
|
+
* initialRouteName: "home",
|
|
183
|
+
* });
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export function createTeardownRouter<T extends NavigatorNode | FlatRouteTree>(
|
|
187
|
+
options: TeardownRouterOptions<T>
|
|
188
|
+
): ComponentType<{ children?: ReactNode }> {
|
|
189
|
+
const {
|
|
190
|
+
routeTree,
|
|
191
|
+
initialRouteName,
|
|
192
|
+
linking,
|
|
193
|
+
theme,
|
|
194
|
+
notFoundComponent: NotFoundComponent,
|
|
195
|
+
navigationContainerProps,
|
|
196
|
+
} = options;
|
|
197
|
+
|
|
198
|
+
// Check if this is a hierarchical route tree
|
|
199
|
+
if (isHierarchicalRouteTree(routeTree)) {
|
|
200
|
+
// Set initial route name if provided
|
|
201
|
+
const treeWithInitial: NavigatorNode = {
|
|
202
|
+
...routeTree,
|
|
203
|
+
initialRouteName: initialRouteName || routeTree.initialRouteName,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
function TeardownRouter(): React.JSX.Element {
|
|
207
|
+
return (
|
|
208
|
+
<NavigationContainer linking={linking} theme={theme} {...navigationContainerProps}>
|
|
209
|
+
<RenderNavigator node={treeWithInitial} />
|
|
210
|
+
</NavigationContainer>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return TeardownRouter;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Legacy flat route tree support
|
|
218
|
+
const flatTree = routeTree as FlatRouteTree;
|
|
219
|
+
const Stack = createNativeStackNavigator();
|
|
220
|
+
const routeEntries = Object.entries(flatTree);
|
|
221
|
+
const firstRoute = routeEntries[0];
|
|
222
|
+
const initialRoute = initialRouteName || (firstRoute ? pathToRouteName(firstRoute[0]) : "index");
|
|
223
|
+
const rootLayout = firstRoute?.[1]?.layout;
|
|
224
|
+
const defaultOptions = rootLayout ? getDefaultScreenOptions(rootLayout.type) : getDefaultScreenOptions("stack");
|
|
225
|
+
|
|
226
|
+
function TeardownRouterLegacy(): React.JSX.Element {
|
|
227
|
+
return (
|
|
228
|
+
<NavigationContainer linking={linking} theme={theme} {...navigationContainerProps}>
|
|
229
|
+
<Stack.Navigator
|
|
230
|
+
initialRouteName={initialRoute}
|
|
231
|
+
screenOptions={{
|
|
232
|
+
...defaultOptions,
|
|
233
|
+
...(rootLayout?.screenOptions || {}),
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
{routeEntries.map(([path, entry]) => {
|
|
237
|
+
const routeName = pathToRouteName(path);
|
|
238
|
+
const screenDef = entry.screen;
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<Stack.Screen
|
|
242
|
+
key={path}
|
|
243
|
+
name={routeName}
|
|
244
|
+
component={screenDef.component}
|
|
245
|
+
options={typeof screenDef.options === "function" ? screenDef.options : screenDef.options}
|
|
246
|
+
/>
|
|
247
|
+
);
|
|
248
|
+
})}
|
|
249
|
+
{NotFoundComponent && (
|
|
250
|
+
<Stack.Screen name="__notFound__" component={NotFoundComponent} options={{ title: "Not Found" }} />
|
|
251
|
+
)}
|
|
252
|
+
</Stack.Navigator>
|
|
253
|
+
</NavigationContainer>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return TeardownRouterLegacy;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Converts a URL path to a route name for React Navigation
|
|
262
|
+
* e.g., "/" -> "index", "/users/:userId" -> "users/[userId]"
|
|
263
|
+
*/
|
|
264
|
+
function pathToRouteName(path: string): string {
|
|
265
|
+
if (path === "/") return "index";
|
|
266
|
+
|
|
267
|
+
return path
|
|
268
|
+
.slice(1) // Remove leading slash
|
|
269
|
+
.replace(/:([^/]+)\?/g, "[[$1]]") // :param? -> [[param]]
|
|
270
|
+
.replace(/:([^/]+)/g, "[$1]") // :param -> [param]
|
|
271
|
+
.replace(/\*/g, "[...catchAll]"); // * -> [...catchAll]
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Type helper to extract route paths from a route tree
|
|
276
|
+
*/
|
|
277
|
+
export type ExtractRoutePaths<T extends NavigatorNode> = T extends NavigatorNode ? keyof T["screens"] & string : never;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Type helper to extract screen definitions from a route tree
|
|
281
|
+
*/
|
|
282
|
+
export type ExtractScreens<T extends NavigatorNode> = T extends NavigatorNode ? T["screens"] : never;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router module exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type { ExtractRoutePaths, ExtractScreens, TeardownRouterOptions } from "./create-router";
|
|
6
|
+
export { createTeardownRouter } from "./create-router";
|
|
7
|
+
// Legacy type aliases for backwards compatibility
|
|
8
|
+
export type {
|
|
9
|
+
DrawerScreenOptions,
|
|
10
|
+
FlatRouteTree,
|
|
11
|
+
FlatRouteTreeEntry,
|
|
12
|
+
FlatRouteTreeEntry as RouteTreeEntry,
|
|
13
|
+
NavigatorNode,
|
|
14
|
+
NavigatorType,
|
|
15
|
+
RouteTree,
|
|
16
|
+
ScreenEntry,
|
|
17
|
+
TabScreenOptions,
|
|
18
|
+
} from "./types";
|
|
19
|
+
export { isFlatRouteTree, isHierarchicalRouteTree } from "./types";
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for hierarchical navigator tree structure
|
|
3
|
+
* Supports nested Stack, Tab, and Drawer navigators
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LayoutDefinition, ScreenDefinition } from "../primitives";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Navigator types supported by the router
|
|
10
|
+
*/
|
|
11
|
+
export type NavigatorType = "stack" | "tabs" | "drawer";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Screen entry in a navigator
|
|
15
|
+
*/
|
|
16
|
+
export interface ScreenEntry {
|
|
17
|
+
/** The screen definition from defineScreen */
|
|
18
|
+
screen: ScreenDefinition;
|
|
19
|
+
/** Path for this screen (used for linking) */
|
|
20
|
+
path?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Navigator node representing a Stack, Tab, or Drawer navigator
|
|
25
|
+
* Can contain screens and nested navigators
|
|
26
|
+
*/
|
|
27
|
+
export interface NavigatorNode {
|
|
28
|
+
/** Navigator type - determines which navigator component to use */
|
|
29
|
+
type: NavigatorType;
|
|
30
|
+
|
|
31
|
+
/** Layout definition containing screen options and configuration */
|
|
32
|
+
layout?: LayoutDefinition;
|
|
33
|
+
|
|
34
|
+
/** Screen options applied to all screens in this navigator */
|
|
35
|
+
screenOptions?: Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
/** Initial route name for this navigator */
|
|
38
|
+
initialRouteName?: string;
|
|
39
|
+
|
|
40
|
+
/** Screens contained directly in this navigator */
|
|
41
|
+
screens: Record<string, ScreenEntry>;
|
|
42
|
+
|
|
43
|
+
/** Nested navigators (e.g., tabs inside a stack) */
|
|
44
|
+
navigators?: Record<string, NavigatorNode>;
|
|
45
|
+
|
|
46
|
+
/** Name of this navigator (used as screen name in parent) */
|
|
47
|
+
name?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Root route tree - the top-level navigator
|
|
52
|
+
*/
|
|
53
|
+
export type RouteTree = NavigatorNode;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Flat route tree entry (legacy format, kept for backwards compatibility)
|
|
57
|
+
*/
|
|
58
|
+
export interface FlatRouteTreeEntry {
|
|
59
|
+
screen: ScreenDefinition;
|
|
60
|
+
layout?: LayoutDefinition;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Flat route tree (legacy format)
|
|
65
|
+
*/
|
|
66
|
+
export type FlatRouteTree = Record<string, FlatRouteTreeEntry>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Type guard to check if a route tree is hierarchical
|
|
70
|
+
*/
|
|
71
|
+
export function isHierarchicalRouteTree(tree: unknown): tree is RouteTree {
|
|
72
|
+
return (
|
|
73
|
+
typeof tree === "object" &&
|
|
74
|
+
tree !== null &&
|
|
75
|
+
"type" in tree &&
|
|
76
|
+
"screens" in tree &&
|
|
77
|
+
typeof (tree as RouteTree).type === "string"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Type guard to check if a route tree is flat (legacy)
|
|
83
|
+
*/
|
|
84
|
+
export function isFlatRouteTree(tree: unknown): tree is FlatRouteTree {
|
|
85
|
+
if (typeof tree !== "object" || tree === null) return false;
|
|
86
|
+
// Flat trees have string keys mapping to {screen, layout?} objects
|
|
87
|
+
const firstValue = Object.values(tree)[0];
|
|
88
|
+
return firstValue && "screen" in firstValue && !("type" in tree);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Options for tab navigator screens
|
|
93
|
+
*/
|
|
94
|
+
export interface TabScreenOptions {
|
|
95
|
+
tabBarLabel?: string;
|
|
96
|
+
tabBarIcon?: (props: { focused: boolean; color: string; size: number }) => React.ReactNode;
|
|
97
|
+
tabBarBadge?: string | number;
|
|
98
|
+
tabBarShowLabel?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Options for drawer navigator screens
|
|
103
|
+
*/
|
|
104
|
+
export interface DrawerScreenOptions {
|
|
105
|
+
drawerLabel?: string;
|
|
106
|
+
drawerIcon?: (props: { focused: boolean; color: string; size: number }) => React.ReactNode;
|
|
107
|
+
}
|