@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.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * createRootLayout - Factory for creating the root layout
3
+ * The root layout is the top-level navigator with no parent
4
+ */
5
+
6
+ import { defineLayout, type EnhancedLayoutDefinition, type NavigatorType } from "../primitives/define-layout";
7
+
8
+ /**
9
+ * Root layout configuration
10
+ */
11
+ export interface RootLayoutConfig<TType extends NavigatorType> {
12
+ /** Navigator type */
13
+ type: TType;
14
+ /** Default screen options for children */
15
+ screenOptions?: object;
16
+ /** Initial route name */
17
+ initialRouteName?: string;
18
+ }
19
+
20
+ /**
21
+ * Create the root layout (no parent)
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * const rootLayout = createRootLayout({ type: 'stack' });
26
+ *
27
+ * // Then add children
28
+ * const routeTree = createRouteTree({
29
+ * root: rootLayout.addChildren([homeScreen, settingsScreen]),
30
+ * });
31
+ * ```
32
+ */
33
+ export function createRootLayout<TType extends NavigatorType>(
34
+ config: RootLayoutConfig<TType>
35
+ ): EnhancedLayoutDefinition<TType, "", "/", Record<string, never>, Record<string, never>, unknown> {
36
+ return defineLayout({
37
+ type: config.type,
38
+ path: "",
39
+ screenOptions: config.screenOptions,
40
+ initialRouteName: config.initialRouteName,
41
+ }) as EnhancedLayoutDefinition<TType, "", "/", Record<string, never>, Record<string, never>, unknown>;
42
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * createRouteTree - Creates a validated route tree from a root layout
3
+ * The route tree is used by the router to render the navigation hierarchy
4
+ */
5
+
6
+ import type { AnyRouteDefinition, ExtractRouteParams, ExtractRoutePaths } from "../types/route-builder-types";
7
+
8
+ /**
9
+ * Route tree structure
10
+ */
11
+ export interface RouteTree<TRoot extends AnyRouteDefinition> {
12
+ /** Root layout */
13
+ root: TRoot;
14
+ /** All route paths (union type) */
15
+ routePaths: ExtractRoutePaths<TRoot>;
16
+ /** Map of path -> params */
17
+ routeParams: ExtractRouteParams<TRoot>;
18
+ }
19
+
20
+ /**
21
+ * Configuration for creating a route tree
22
+ */
23
+ export interface CreateRouteTreeConfig<TRoot extends AnyRouteDefinition> {
24
+ /** Root layout with children */
25
+ root: TRoot;
26
+ }
27
+
28
+ /**
29
+ * Create a validated route tree from root layout
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * const routeTree = createRouteTree({
34
+ * root: rootLayout.addChildren([
35
+ * usersLayout.addChildren([userProfile, userPosts]),
36
+ * settingsScreen,
37
+ * ]),
38
+ * });
39
+ * ```
40
+ */
41
+ export function createRouteTree<TRoot extends AnyRouteDefinition>(
42
+ config: CreateRouteTreeConfig<TRoot>
43
+ ): RouteTree<TRoot> {
44
+ // Validate tree structure at runtime
45
+ validateRouteTree(config.root);
46
+
47
+ return {
48
+ root: config.root,
49
+ routePaths: extractRoutePaths(config.root) as ExtractRoutePaths<TRoot>,
50
+ routeParams: extractRouteParams(config.root) as ExtractRouteParams<TRoot>,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Validate route tree structure
56
+ * Checks for duplicate paths and other issues
57
+ */
58
+ function validateRouteTree(node: AnyRouteDefinition, seen = new Set<string>()): void {
59
+ const fullPath = node.fullPath;
60
+
61
+ // Skip empty paths (root layout)
62
+ if (fullPath && fullPath !== "/") {
63
+ if (seen.has(fullPath)) {
64
+ throw new Error(`Duplicate route path: ${fullPath}`);
65
+ }
66
+ seen.add(fullPath);
67
+ }
68
+
69
+ // Recursively validate children
70
+ if ("children" in node && Array.isArray(node.children)) {
71
+ for (const child of node.children) {
72
+ validateRouteTree(child as AnyRouteDefinition, seen);
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Extract all route paths from a route tree
79
+ */
80
+ function extractRoutePaths(node: AnyRouteDefinition): string[] {
81
+ const paths: string[] = [];
82
+
83
+ if (node.fullPath) {
84
+ paths.push(node.fullPath);
85
+ }
86
+
87
+ if ("children" in node && Array.isArray(node.children)) {
88
+ for (const child of node.children) {
89
+ paths.push(...extractRoutePaths(child as AnyRouteDefinition));
90
+ }
91
+ }
92
+
93
+ return paths;
94
+ }
95
+
96
+ /**
97
+ * Extract route params map from a route tree
98
+ */
99
+ function extractRouteParams(node: AnyRouteDefinition): Record<string, unknown> {
100
+ const params: Record<string, unknown> = {};
101
+
102
+ if (node.fullPath) {
103
+ params[node.fullPath] = node.allParams;
104
+ }
105
+
106
+ if ("children" in node && Array.isArray(node.children)) {
107
+ for (const child of node.children) {
108
+ Object.assign(params, extractRouteParams(child as AnyRouteDefinition));
109
+ }
110
+ }
111
+
112
+ return params;
113
+ }
114
+
115
+ /**
116
+ * Get all route definitions from a route tree as a flat array
117
+ */
118
+ export function flattenRouteTree(node: AnyRouteDefinition): AnyRouteDefinition[] {
119
+ const routes: AnyRouteDefinition[] = [node];
120
+
121
+ if ("children" in node && Array.isArray(node.children)) {
122
+ for (const child of node.children) {
123
+ routes.push(...flattenRouteTree(child as AnyRouteDefinition));
124
+ }
125
+ }
126
+
127
+ return routes;
128
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Route builder module for @teardown/navigation
3
+ * Provides utilities for building type-safe route trees
4
+ */
5
+
6
+ export { createRootLayout, type RootLayoutConfig } from "./create-root-layout";
7
+ export { type CreateRouteTreeConfig, createRouteTree, flattenRouteTree, type RouteTree } from "./create-route-tree";
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Type-level tests for route builder types
3
+ * These tests verify that the type utilities correctly compute paths and params
4
+ *
5
+ * Note: These are compile-time type tests. The runtime assertions are minimal
6
+ * because the main goal is to verify TypeScript type inference is correct.
7
+ */
8
+
9
+ import { describe, expect, it } from "bun:test";
10
+ import type { AccumulateParams, ComputeFullPath, StripRouteGroups } from "../types/route-builder-types";
11
+
12
+ // Helper type for testing - checks if two types are exactly equal
13
+ type Equals<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
14
+
15
+ // Type assertion helper - verifies type at compile time
16
+ function assertType<T>(_value: T): void {}
17
+
18
+ describe("Route Builder Types - Compile Time", () => {
19
+ describe("StripRouteGroups", () => {
20
+ it("strips route group prefix", () => {
21
+ type Result = StripRouteGroups<"(auth)/login">;
22
+ const isEqual: Equals<Result, "login"> = true;
23
+ expect(isEqual).toBe(true);
24
+ });
25
+
26
+ it("strips standalone route group", () => {
27
+ type Result = StripRouteGroups<"(auth)">;
28
+ const isEqual: Equals<Result, ""> = true;
29
+ expect(isEqual).toBe(true);
30
+ });
31
+
32
+ it("preserves paths without route groups", () => {
33
+ type Result = StripRouteGroups<"users">;
34
+ const isEqual: Equals<Result, "users"> = true;
35
+ expect(isEqual).toBe(true);
36
+ });
37
+
38
+ it("strips nested route groups", () => {
39
+ type Result = StripRouteGroups<"(admin)/(dashboard)/settings">;
40
+ const isEqual: Equals<Result, "settings"> = true;
41
+ expect(isEqual).toBe(true);
42
+ });
43
+ });
44
+
45
+ describe("ComputeFullPath", () => {
46
+ it("computes root path for empty path with no parent", () => {
47
+ type Result = ComputeFullPath<"", undefined>;
48
+ const isEqual: Equals<Result, "/"> = true;
49
+ expect(isEqual).toBe(true);
50
+ });
51
+
52
+ it("computes full path for simple segment with no parent", () => {
53
+ type Result = ComputeFullPath<"users", undefined>;
54
+ const isEqual: Equals<Result, "/users"> = true;
55
+ expect(isEqual).toBe(true);
56
+ });
57
+
58
+ it("computes full path with parent", () => {
59
+ type Parent = { fullPath: "/users" };
60
+ type Result = ComputeFullPath<"[userId]", Parent>;
61
+ const isEqual: Equals<Result, "/users/[userId]"> = true;
62
+ expect(isEqual).toBe(true);
63
+ });
64
+
65
+ it("computes full path with root parent", () => {
66
+ type Parent = { fullPath: "/" };
67
+ type Result = ComputeFullPath<"users", Parent>;
68
+ const isEqual: Equals<Result, "/users"> = true;
69
+ expect(isEqual).toBe(true);
70
+ });
71
+
72
+ it("strips route groups from computed path", () => {
73
+ type Parent = { fullPath: "/" };
74
+ type Result = ComputeFullPath<"(auth)/login", Parent>;
75
+ const isEqual: Equals<Result, "/login"> = true;
76
+ expect(isEqual).toBe(true);
77
+ });
78
+ });
79
+
80
+ describe("AccumulateParams", () => {
81
+ it("extracts params from single segment", () => {
82
+ type Result = AccumulateParams<"[userId]", undefined>;
83
+ assertType<Result>({ userId: "123" });
84
+ expect(true).toBe(true);
85
+ });
86
+
87
+ it("accumulates params from parent", () => {
88
+ type Parent = { allParams: { userId: string } };
89
+ type Result = AccumulateParams<"[postId]", Parent>;
90
+ assertType<Result>({ userId: "123", postId: "456" });
91
+ expect(true).toBe(true);
92
+ });
93
+
94
+ it("returns empty for static paths", () => {
95
+ type Result = AccumulateParams<"about", undefined>;
96
+ const result: Result = {};
97
+ expect(Object.keys(result).length).toBe(0);
98
+ });
99
+ });
100
+ });
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Runtime tests for route builder
3
+ */
4
+
5
+ import { describe, expect, it } from "bun:test";
6
+ import { defineLayout, defineScreen } from "../primitives";
7
+ import { createRootLayout } from "./create-root-layout";
8
+ import { createRouteTree, flattenRouteTree } from "./create-route-tree";
9
+
10
+ // Dummy component for tests
11
+ const DummyComponent = () => null;
12
+
13
+ describe("Route Builder Runtime", () => {
14
+ describe("createRootLayout", () => {
15
+ it("creates a root layout with fullPath /", () => {
16
+ const root = createRootLayout({ type: "stack" });
17
+
18
+ expect(root.path).toBe("");
19
+ expect(root.fullPath).toBe("/");
20
+ expect(root.__brand).toBe("TeardownLayout");
21
+ });
22
+
23
+ it("supports different navigator types", () => {
24
+ const stack = createRootLayout({ type: "stack" });
25
+ const tabs = createRootLayout({ type: "tabs" });
26
+ const drawer = createRootLayout({ type: "drawer" });
27
+
28
+ expect(stack.type).toBe("stack");
29
+ expect(tabs.type).toBe("tabs");
30
+ expect(drawer.type).toBe("drawer");
31
+ });
32
+
33
+ it("passes through screen options", () => {
34
+ const root = createRootLayout({
35
+ type: "stack",
36
+ screenOptions: { headerShown: false },
37
+ });
38
+
39
+ expect(root.screenOptions).toEqual({ headerShown: false });
40
+ });
41
+ });
42
+
43
+ describe("defineLayout with path", () => {
44
+ it("computes fullPath correctly", () => {
45
+ const root = createRootLayout({ type: "stack" });
46
+ const users = defineLayout({
47
+ type: "stack",
48
+ path: "users",
49
+ getParentRoute: () => root,
50
+ });
51
+
52
+ expect(users.fullPath).toBe("/users");
53
+ });
54
+
55
+ it("strips route groups from fullPath", () => {
56
+ const root = createRootLayout({ type: "stack" });
57
+ const auth = defineLayout({
58
+ type: "stack",
59
+ path: "(auth)",
60
+ getParentRoute: () => root,
61
+ });
62
+ const login = defineScreen({
63
+ path: "login",
64
+ getParentRoute: () => auth,
65
+ component: DummyComponent,
66
+ });
67
+
68
+ expect(login.fullPath).toBe("/login");
69
+ });
70
+
71
+ it("chains nested layouts correctly", () => {
72
+ const root = createRootLayout({ type: "stack" });
73
+ const users = defineLayout({
74
+ type: "stack",
75
+ path: "users",
76
+ getParentRoute: () => root,
77
+ });
78
+ const settings = defineLayout({
79
+ type: "tabs",
80
+ path: "settings",
81
+ getParentRoute: () => users,
82
+ });
83
+
84
+ expect(settings.fullPath).toBe("/users/settings");
85
+ });
86
+ });
87
+
88
+ describe("defineScreen with path", () => {
89
+ it("computes fullPath from parent chain", () => {
90
+ const root = createRootLayout({ type: "stack" });
91
+ const users = defineLayout({
92
+ type: "stack",
93
+ path: "users",
94
+ getParentRoute: () => root,
95
+ });
96
+ const profile = defineScreen({
97
+ path: "[userId]",
98
+ getParentRoute: () => users,
99
+ component: DummyComponent,
100
+ });
101
+
102
+ expect(profile.fullPath).toBe("/users/[userId]");
103
+ });
104
+
105
+ it("creates screen without parent at root", () => {
106
+ const home = defineScreen({
107
+ path: "home",
108
+ component: DummyComponent,
109
+ });
110
+
111
+ expect(home.fullPath).toBe("/home");
112
+ });
113
+
114
+ it("handles nested params correctly", () => {
115
+ const root = createRootLayout({ type: "stack" });
116
+ const users = defineLayout({
117
+ type: "stack",
118
+ path: "users/[userId]",
119
+ getParentRoute: () => root,
120
+ });
121
+ const posts = defineScreen({
122
+ path: "posts/[postId]",
123
+ getParentRoute: () => users,
124
+ component: DummyComponent,
125
+ });
126
+
127
+ expect(posts.fullPath).toBe("/users/[userId]/posts/[postId]");
128
+ });
129
+ });
130
+
131
+ describe("addChildren", () => {
132
+ it("returns new layout with children", () => {
133
+ const root = createRootLayout({ type: "stack" });
134
+ const screen1 = defineScreen({ path: "a", component: DummyComponent });
135
+ const screen2 = defineScreen({ path: "b", component: DummyComponent });
136
+
137
+ const withChildren = root.addChildren([screen1, screen2]);
138
+
139
+ expect(withChildren.children).toHaveLength(2);
140
+ expect(withChildren.children[0].path).toBe("a");
141
+ expect(withChildren.children[1].path).toBe("b");
142
+ });
143
+
144
+ it("preserves original layout properties", () => {
145
+ const root = createRootLayout({
146
+ type: "tabs",
147
+ screenOptions: { headerShown: false },
148
+ });
149
+ const screen = defineScreen({ path: "home", component: DummyComponent });
150
+
151
+ const withChildren = root.addChildren([screen]);
152
+
153
+ expect(withChildren.type).toBe("tabs");
154
+ expect(withChildren.screenOptions).toEqual({ headerShown: false });
155
+ });
156
+
157
+ it("supports nested addChildren", () => {
158
+ const root = createRootLayout({ type: "stack" });
159
+ const usersLayout = defineLayout({
160
+ type: "stack",
161
+ path: "users",
162
+ getParentRoute: () => root,
163
+ });
164
+ const profile = defineScreen({
165
+ path: "[userId]",
166
+ getParentRoute: () => usersLayout,
167
+ component: DummyComponent,
168
+ });
169
+
170
+ const tree = root.addChildren([usersLayout.addChildren([profile])]);
171
+
172
+ expect(tree.children).toHaveLength(1);
173
+ expect((tree.children[0] as typeof usersLayout).children).toHaveLength(1);
174
+ });
175
+ });
176
+
177
+ describe("createRouteTree", () => {
178
+ it("creates a route tree from root", () => {
179
+ const root = createRootLayout({ type: "stack" });
180
+ const home = defineScreen({ path: "home", component: DummyComponent });
181
+
182
+ const tree = createRouteTree({
183
+ root: root.addChildren([home]),
184
+ });
185
+
186
+ expect(tree.root).toBeDefined();
187
+ expect(Array.isArray(tree.routePaths)).toBe(true);
188
+ });
189
+
190
+ it("validates no duplicate paths", () => {
191
+ const root = createRootLayout({ type: "stack" });
192
+ const screen1 = defineScreen({ path: "same", component: DummyComponent });
193
+ const screen2 = defineScreen({ path: "same", component: DummyComponent });
194
+
195
+ expect(() => {
196
+ createRouteTree({ root: root.addChildren([screen1, screen2]) });
197
+ }).toThrow("Duplicate route path");
198
+ });
199
+
200
+ it("extracts all route paths", () => {
201
+ const root = createRootLayout({ type: "stack" });
202
+ const home = defineScreen({ path: "home", component: DummyComponent });
203
+ const about = defineScreen({ path: "about", component: DummyComponent });
204
+
205
+ const tree = createRouteTree({
206
+ root: root.addChildren([home, about]),
207
+ });
208
+
209
+ const paths = tree.routePaths as string[];
210
+ expect(paths).toContain("/");
211
+ expect(paths).toContain("/home");
212
+ expect(paths).toContain("/about");
213
+ });
214
+ });
215
+
216
+ describe("flattenRouteTree", () => {
217
+ it("flattens nested route tree", () => {
218
+ const root = createRootLayout({ type: "stack" });
219
+ const usersLayout = defineLayout({
220
+ type: "stack",
221
+ path: "users",
222
+ getParentRoute: () => root,
223
+ });
224
+ const profile = defineScreen({
225
+ path: "[userId]",
226
+ getParentRoute: () => usersLayout,
227
+ component: DummyComponent,
228
+ });
229
+
230
+ const tree = root.addChildren([usersLayout.addChildren([profile])]);
231
+ const flat = flattenRouteTree(tree);
232
+
233
+ expect(flat.length).toBe(3); // root, usersLayout, profile
234
+ });
235
+ });
236
+
237
+ describe("scoped hooks", () => {
238
+ it("provides useParams hook on screen", () => {
239
+ const screen = defineScreen({
240
+ path: "[userId]",
241
+ component: DummyComponent,
242
+ });
243
+
244
+ expect(typeof screen.useParams).toBe("function");
245
+ });
246
+
247
+ it("provides useNavigate hook on screen", () => {
248
+ const screen = defineScreen({
249
+ path: "home",
250
+ component: DummyComponent,
251
+ });
252
+
253
+ expect(typeof screen.useNavigate).toBe("function");
254
+ });
255
+
256
+ it("provides useRouteContext hook on screen", () => {
257
+ const screen = defineScreen({
258
+ path: "home",
259
+ component: DummyComponent,
260
+ });
261
+
262
+ expect(typeof screen.useRouteContext).toBe("function");
263
+ });
264
+
265
+ it("provides hooks on layout", () => {
266
+ const layout = defineLayout({
267
+ type: "stack",
268
+ path: "users",
269
+ });
270
+
271
+ expect(typeof layout.useParams).toBe("function");
272
+ expect(typeof layout.useNavigate).toBe("function");
273
+ expect(typeof layout.useRouteContext).toBe("function");
274
+ });
275
+ });
276
+ });
@@ -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
 
@@ -121,6 +125,31 @@ export interface TeardownRouterOptions<T extends NavigatorNode | FlatRouteTree =
121
125
  navigationContainerProps?: Omit<React.ComponentProps<typeof NavigationContainer>, "children">;
122
126
  }
123
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
+
124
153
  /**
125
154
  * Creates a navigator component based on type
126
155
  */
@@ -335,3 +364,108 @@ export type ExtractRoutePaths<T extends NavigatorNode> = T extends NavigatorNode
335
364
  * Type helper to extract screen definitions from a route tree
336
365
  */
337
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
+ }