@teardown/navigation 2.0.79 → 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.
@@ -8,6 +8,10 @@ import { NavigationContainer } from "@react-navigation/native";
8
8
  import { createNativeStackNavigator } from "@react-navigation/native-stack";
9
9
  import type React from "react";
10
10
  import type { ComponentType, ReactNode } from "react";
11
+ import { useEffect } from "react";
12
+ import { initializeScopedHooks } from "../hooks/scoped-hooks";
13
+ import type { RouteTree } from "../route-builder";
14
+ import type { AnyRouteDefinition } from "../types/route-builder-types";
11
15
  import type { FlatRouteTree, NavigatorNode, ScreenEntry } from "./types";
12
16
  import { isHierarchicalRouteTree } from "./types";
13
17
 
@@ -15,11 +19,16 @@ import { isHierarchicalRouteTree } from "./types";
15
19
  let createBottomTabNavigator: (() => ReturnType<typeof createNativeStackNavigator>) | null = null;
16
20
  let createDrawerNavigator: (() => ReturnType<typeof createNativeStackNavigator>) | null = null;
17
21
 
22
+ // Track which navigators are available
23
+ let bottomTabsAvailable = false;
24
+ let drawerAvailable = false;
25
+
18
26
  // Try to load optional dependencies
19
27
  try {
20
28
  // eslint-disable-next-line @typescript-eslint/no-require-imports
21
29
  const bottomTabs = require("@react-navigation/bottom-tabs");
22
30
  createBottomTabNavigator = bottomTabs.createBottomTabNavigator;
31
+ bottomTabsAvailable = true;
23
32
  } catch {
24
33
  // Bottom tabs not installed
25
34
  }
@@ -28,10 +37,57 @@ try {
28
37
  // eslint-disable-next-line @typescript-eslint/no-require-imports
29
38
  const drawer = require("@react-navigation/drawer");
30
39
  createDrawerNavigator = drawer.createDrawerNavigator;
40
+ drawerAvailable = true;
31
41
  } catch {
32
42
  // Drawer not installed
33
43
  }
34
44
 
45
+ /**
46
+ * Recursively checks a navigator node tree for required navigator types
47
+ */
48
+ function collectRequiredNavigatorTypes(node: NavigatorNode, types: Set<"tabs" | "drawer">): void {
49
+ if (node.type === "tabs") {
50
+ types.add("tabs");
51
+ } else if (node.type === "drawer") {
52
+ types.add("drawer");
53
+ }
54
+
55
+ // Check nested navigators
56
+ if (node.navigators) {
57
+ for (const nestedNode of Object.values(node.navigators)) {
58
+ collectRequiredNavigatorTypes(nestedNode, types);
59
+ }
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Validates that required navigator dependencies are installed and throws if not
65
+ */
66
+ function validateNavigatorDependencies(routeTree: NavigatorNode): void {
67
+ const requiredTypes = new Set<"tabs" | "drawer">();
68
+ collectRequiredNavigatorTypes(routeTree, requiredTypes);
69
+
70
+ const missingDeps: string[] = [];
71
+
72
+ if (requiredTypes.has("tabs") && !bottomTabsAvailable) {
73
+ missingDeps.push(
74
+ "Your route tree uses tabs navigation but @react-navigation/bottom-tabs is not installed.\n" +
75
+ " Install it with: npm install @react-navigation/bottom-tabs"
76
+ );
77
+ }
78
+
79
+ if (requiredTypes.has("drawer") && !drawerAvailable) {
80
+ missingDeps.push(
81
+ "Your route tree uses drawer navigation but @react-navigation/drawer is not installed.\n" +
82
+ " Install it with: npm install @react-navigation/drawer react-native-gesture-handler react-native-reanimated"
83
+ );
84
+ }
85
+
86
+ if (missingDeps.length > 0) {
87
+ throw new Error(`[@teardown/navigation] Missing required navigation dependencies:\n\n${missingDeps.join("\n\n")}`);
88
+ }
89
+ }
90
+
35
91
  /**
36
92
  * Options for creating a Teardown router
37
93
  */
@@ -69,6 +125,31 @@ export interface TeardownRouterOptions<T extends NavigatorNode | FlatRouteTree =
69
125
  navigationContainerProps?: Omit<React.ComponentProps<typeof NavigationContainer>, "children">;
70
126
  }
71
127
 
128
+ /**
129
+ * Options for creating a Teardown router from a RouteTree
130
+ */
131
+ export interface RouteTreeRouterOptions<TRoot extends AnyRouteDefinition> {
132
+ /**
133
+ * The route tree created with createRouteTree()
134
+ */
135
+ routeTree: RouteTree<TRoot>;
136
+
137
+ /**
138
+ * Deep linking configuration
139
+ */
140
+ linking?: LinkingOptions<Record<string, unknown>>;
141
+
142
+ /**
143
+ * Navigation theme
144
+ */
145
+ theme?: Theme;
146
+
147
+ /**
148
+ * Custom navigation container wrapper
149
+ */
150
+ navigationContainerProps?: Omit<React.ComponentProps<typeof NavigationContainer>, "children">;
151
+ }
152
+
72
153
  /**
73
154
  * Creates a navigator component based on type
74
155
  */
@@ -76,18 +157,18 @@ function createNavigator(type: "stack" | "tabs" | "drawer") {
76
157
  switch (type) {
77
158
  case "tabs":
78
159
  if (!createBottomTabNavigator) {
79
- console.warn(
80
- "[@teardown/navigation] Tab navigator requested but @react-navigation/bottom-tabs is not installed. Falling back to stack."
160
+ throw new Error(
161
+ "[@teardown/navigation] Tab navigator requested but @react-navigation/bottom-tabs is not installed.\n" +
162
+ "Install it with: npm install @react-navigation/bottom-tabs"
81
163
  );
82
- return createNativeStackNavigator();
83
164
  }
84
165
  return createBottomTabNavigator();
85
166
  case "drawer":
86
167
  if (!createDrawerNavigator) {
87
- console.warn(
88
- "[@teardown/navigation] Drawer navigator requested but @react-navigation/drawer is not installed. Falling back to stack."
168
+ throw new Error(
169
+ "[@teardown/navigation] Drawer navigator requested but @react-navigation/drawer is not installed.\n" +
170
+ "Install it with: npm install @react-navigation/drawer react-native-gesture-handler react-native-reanimated"
89
171
  );
90
- return createNativeStackNavigator();
91
172
  }
92
173
  return createDrawerNavigator();
93
174
  default:
@@ -197,6 +278,9 @@ export function createTeardownRouter<T extends NavigatorNode | FlatRouteTree>(
197
278
 
198
279
  // Check if this is a hierarchical route tree
199
280
  if (isHierarchicalRouteTree(routeTree)) {
281
+ // Validate that required navigator dependencies are installed (warns early)
282
+ validateNavigatorDependencies(routeTree);
283
+
200
284
  // Set initial route name if provided
201
285
  const treeWithInitial: NavigatorNode = {
202
286
  ...routeTree,
@@ -280,3 +364,108 @@ export type ExtractRoutePaths<T extends NavigatorNode> = T extends NavigatorNode
280
364
  * Type helper to extract screen definitions from a route tree
281
365
  */
282
366
  export type ExtractScreens<T extends NavigatorNode> = T extends NavigatorNode ? T["screens"] : never;
367
+
368
+ /**
369
+ * Creates a Teardown Router from a RouteTree (enhanced API)
370
+ *
371
+ * This is the new API that uses the route builder pattern with
372
+ * createRootLayout, defineLayout, and defineScreen.
373
+ *
374
+ * @example
375
+ * ```tsx
376
+ * import { createTeardownRouterFromTree, createRouteTree, createRootLayout, defineScreen } from "@teardown/navigation";
377
+ *
378
+ * const root = createRootLayout({ type: 'stack' });
379
+ * const home = defineScreen({ path: 'home', component: HomeScreen, getParentRoute: () => root });
380
+ *
381
+ * const routeTree = createRouteTree({
382
+ * root: root.addChildren([home]),
383
+ * });
384
+ *
385
+ * export const Router = createTeardownRouterFromTree({ routeTree });
386
+ * ```
387
+ */
388
+ export function createTeardownRouterFromTree<TRoot extends AnyRouteDefinition>(
389
+ options: RouteTreeRouterOptions<TRoot>
390
+ ): ComponentType<{ children?: ReactNode }> {
391
+ const { routeTree, linking, theme, navigationContainerProps } = options;
392
+
393
+ function TeardownRouterFromTree(): React.JSX.Element {
394
+ // Initialize scoped hooks with React Navigation
395
+ useEffect(() => {
396
+ try {
397
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
398
+ const { useRoute, useNavigation } = require("@react-navigation/native");
399
+ initializeScopedHooks(useRoute, useNavigation);
400
+ } catch {
401
+ console.warn("[@teardown/navigation] @react-navigation/native not found");
402
+ }
403
+ }, []);
404
+
405
+ // Build navigator tree from route tree
406
+ const navigatorNode = buildNavigatorFromRouteTree(routeTree.root);
407
+
408
+ return (
409
+ <NavigationContainer linking={linking} theme={theme} {...navigationContainerProps}>
410
+ <RenderNavigator node={navigatorNode} />
411
+ </NavigationContainer>
412
+ );
413
+ }
414
+
415
+ return TeardownRouterFromTree;
416
+ }
417
+
418
+ /**
419
+ * Builds a NavigatorNode from a RouteTree root
420
+ */
421
+ function buildNavigatorFromRouteTree(root: AnyRouteDefinition): NavigatorNode {
422
+ const isLayout = "__brand" in root && root.__brand === "TeardownLayout";
423
+
424
+ if (!isLayout) {
425
+ // Single screen, wrap in a stack
426
+ return {
427
+ type: "stack",
428
+ screens: {
429
+ [(root as { path?: string }).path || "index"]: {
430
+ screen: root as unknown as ScreenEntry["screen"],
431
+ path: (root as { fullPath?: string }).fullPath || "/",
432
+ },
433
+ },
434
+ };
435
+ }
436
+
437
+ const layout = root as unknown as {
438
+ type: "stack" | "tabs" | "drawer";
439
+ screenOptions?: object;
440
+ initialRouteName?: string;
441
+ children: AnyRouteDefinition[];
442
+ };
443
+
444
+ const screens: Record<string, ScreenEntry> = {};
445
+ const navigators: Record<string, NavigatorNode> = {};
446
+
447
+ for (const child of layout.children || []) {
448
+ const childBrand = "__brand" in child ? child.__brand : undefined;
449
+ const childPath = (child as { path?: string }).path || "index";
450
+ const childFullPath = (child as { fullPath?: string }).fullPath || "/";
451
+
452
+ if (childBrand === "TeardownLayout") {
453
+ // Nested navigator
454
+ navigators[childPath] = buildNavigatorFromRouteTree(child);
455
+ } else {
456
+ // Screen
457
+ screens[childPath] = {
458
+ screen: child as unknown as ScreenEntry["screen"],
459
+ path: childFullPath,
460
+ };
461
+ }
462
+ }
463
+
464
+ return {
465
+ type: layout.type,
466
+ screens,
467
+ navigators: Object.keys(navigators).length > 0 ? navigators : undefined,
468
+ screenOptions: layout.screenOptions as Record<string, unknown> | undefined,
469
+ initialRouteName: layout.initialRouteName,
470
+ };
471
+ }
@@ -2,8 +2,13 @@
2
2
  * Router module exports
3
3
  */
4
4
 
5
- export type { ExtractRoutePaths, ExtractScreens, TeardownRouterOptions } from "./create-router";
6
- export { createTeardownRouter } from "./create-router";
5
+ export type {
6
+ ExtractRoutePaths,
7
+ ExtractScreens,
8
+ RouteTreeRouterOptions,
9
+ TeardownRouterOptions,
10
+ } from "./create-router";
11
+ export { createTeardownRouter, createTeardownRouterFromTree } from "./create-router";
7
12
  // Legacy type aliases for backwards compatibility
8
13
  export type {
9
14
  DrawerScreenOptions,
@@ -2,6 +2,18 @@
2
2
  * Type utilities and definitions for @teardown/navigation
3
3
  */
4
4
 
5
+ // Route builder types
6
+ export type {
7
+ AccumulateParams,
8
+ AnyRouteDefinition,
9
+ ComputeFullPath,
10
+ ExtractRouteParams,
11
+ ExtractRoutePaths,
12
+ MergeRouteParams,
13
+ ParamsForPath,
14
+ RouteDefinitionBase,
15
+ StripRouteGroups,
16
+ } from "./route-builder-types";
5
17
  // Route type definitions
6
18
  export type {
7
19
  NavigationState,
@@ -22,7 +34,9 @@ export type {
22
34
  InferParams,
23
35
  IsCatchAllSegment,
24
36
  IsDynamicSegment,
37
+ IsEmptyParams,
25
38
  IsOptionalSegment,
26
39
  NavigateArgs,
27
40
  Simplify,
41
+ ToReactNavigationPath,
28
42
  } from "./type-utils";
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Route builder type utilities for @teardown/navigation
3
+ * Enables compile-time path computation and param accumulation
4
+ */
5
+
6
+ import type { InferParams, Simplify } from "./type-utils";
7
+
8
+ /**
9
+ * Compute full path from parent chain
10
+ * Handles route groups by stripping (groupName) segments
11
+ */
12
+ export type ComputeFullPath<TPath extends string, TParent> = TParent extends {
13
+ fullPath: infer P extends string;
14
+ }
15
+ ? P extends "/"
16
+ ? `/${StripRouteGroups<TPath>}`
17
+ : StripRouteGroups<TPath> extends ""
18
+ ? P
19
+ : `${P}/${StripRouteGroups<TPath>}`
20
+ : StripRouteGroups<TPath> extends ""
21
+ ? "/"
22
+ : `/${StripRouteGroups<TPath>}`;
23
+
24
+ /**
25
+ * Strip route group segments from path
26
+ * (auth)/login -> login
27
+ * (auth) -> ""
28
+ */
29
+ export type StripRouteGroups<TPath extends string> = TPath extends `(${string})/${infer Rest}`
30
+ ? StripRouteGroups<Rest>
31
+ : TPath extends `(${string})`
32
+ ? ""
33
+ : TPath;
34
+
35
+ /**
36
+ * Accumulate params from parent chain + this route
37
+ */
38
+ export type AccumulateParams<TPath extends string, TParent> = Simplify<
39
+ InferParams<TPath> & (TParent extends { allParams: infer P } ? P : {})
40
+ >;
41
+
42
+ /**
43
+ * Base interface for all route definitions (screens and layouts)
44
+ */
45
+ export interface RouteDefinitionBase<
46
+ TPath extends string = string,
47
+ TFullPath extends string = string,
48
+ TParams = unknown,
49
+ TAllParams = unknown,
50
+ TContext = unknown,
51
+ > {
52
+ /** This route segment path */
53
+ readonly path: TPath;
54
+ /** Full path from root */
55
+ readonly fullPath: TFullPath;
56
+ /** Params from this route only */
57
+ readonly params: TParams;
58
+ /** Accumulated params from parent chain */
59
+ readonly allParams: TAllParams;
60
+ /** Loader context type */
61
+ readonly context: TContext;
62
+ }
63
+
64
+ /**
65
+ * Type for any route definition (screen or layout)
66
+ */
67
+ export type AnyRouteDefinition = RouteDefinitionBase<string, string, unknown, unknown, unknown>;
68
+
69
+ /**
70
+ * Extract all route paths from a route tree
71
+ */
72
+ export type ExtractRoutePaths<T> = T extends { fullPath: infer P extends string }
73
+ ?
74
+ | P
75
+ | (T extends { children: infer C }
76
+ ? C extends readonly (infer Child)[]
77
+ ? ExtractRoutePaths<Child>
78
+ : never
79
+ : never)
80
+ : never;
81
+
82
+ /**
83
+ * Extract route params map from a route tree
84
+ */
85
+ export type ExtractRouteParams<T> = T extends { fullPath: infer P extends string; allParams: infer Params }
86
+ ? { [K in P]: Params } & (T extends { children: infer C }
87
+ ? C extends readonly (infer Child)[]
88
+ ? ExtractRouteParams<Child>
89
+ : {}
90
+ : {})
91
+ : {};
92
+
93
+ /**
94
+ * Merge two route param maps
95
+ */
96
+ export type MergeRouteParams<A, B> = Simplify<A & B>;
97
+
98
+ /**
99
+ * Get params for a specific path from a route tree
100
+ */
101
+ export type ParamsForPath<TTree, TPath extends string> = TPath extends keyof ExtractRouteParams<TTree>
102
+ ? ExtractRouteParams<TTree>[TPath]
103
+ : never;
@@ -11,19 +11,46 @@ export type Simplify<T> = { [K in keyof T]: T[K] } & {};
11
11
 
12
12
  /**
13
13
  * Extracts parameter from a single path segment
14
+ * Supports both [param] and :param syntax
14
15
  *
15
- * - [userId] -> { userId: string }
16
- * - [...slug] -> { slug: string[] }
17
- * - [section]? -> { section?: string }
16
+ * - [userId] or :userId -> { userId: string }
17
+ * - [...slug] or *slug -> { slug: string[] }
18
+ * - [section]? or :section? -> { section?: string }
18
19
  * - about -> Record<string, never>
19
20
  */
20
- export type ExtractParam<Segment extends string> = Segment extends `[...${infer Rest}]`
21
- ? { [K in Rest]: string[] }
22
- : Segment extends `[${infer Param}]?`
23
- ? { [K in Param]?: string }
24
- : Segment extends `[${infer Param}]`
25
- ? { [K in Param]: string }
26
- : Record<string, never>;
21
+ export type ExtractParam<Segment extends string> =
22
+ // Catch-all: [...slug] or *slug
23
+ Segment extends `[...${infer Rest}]`
24
+ ? { [K in Rest]: string[] }
25
+ : Segment extends `*${infer Rest}`
26
+ ? { [K in Rest]: string[] }
27
+ : // Optional: [param]? or :param?
28
+ Segment extends `[${infer Param}]?`
29
+ ? { [K in Param]?: string }
30
+ : Segment extends `:${infer Param}?`
31
+ ? { [K in Param]?: string }
32
+ : // Required: [param] or :param
33
+ Segment extends `[${infer Param}]`
34
+ ? { [K in Param]: string }
35
+ : Segment extends `:${infer Param}`
36
+ ? { [K in Param]: string }
37
+ : // Static segment
38
+ Record<string, never>;
39
+
40
+ /**
41
+ * Check if params object is empty (no required params)
42
+ */
43
+ export type IsEmptyParams<T> = keyof T extends never ? true : false;
44
+
45
+ /**
46
+ * Convert [param] syntax to :param syntax for React Navigation
47
+ */
48
+ export type ToReactNavigationPath<TPath extends string> =
49
+ TPath extends `${infer Before}[...${infer Param}]${infer After}`
50
+ ? ToReactNavigationPath<`${Before}*${Param}${After}`>
51
+ : TPath extends `${infer Before}[${infer Param}]${infer After}`
52
+ ? ToReactNavigationPath<`${Before}:${Param}${After}`>
53
+ : TPath;
27
54
 
28
55
  /**
29
56
  * Recursively extracts all params from a path