@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teardown/navigation",
3
- "version": "2.0.70",
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.70",
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
+ }