@teardown/navigation-metro 2.0.52 → 2.0.56
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/dist/generator/index.d.ts +5 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +12 -0
- package/dist/generator/route-generator.d.ts +37 -0
- package/dist/generator/route-generator.d.ts.map +1 -0
- package/dist/generator/route-generator.js +179 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +103 -0
- package/dist/scanner/file-scanner.d.ts +62 -0
- package/dist/scanner/file-scanner.d.ts.map +1 -0
- package/dist/scanner/file-scanner.js +250 -0
- package/dist/scanner/index.d.ts +5 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +12 -0
- package/{src/validator/index.ts → dist/validator/index.d.ts} +1 -1
- package/dist/validator/index.d.ts.map +1 -0
- package/dist/validator/index.js +8 -0
- package/dist/validator/route-validator.d.ts +15 -0
- package/dist/validator/route-validator.d.ts.map +1 -0
- package/dist/validator/route-validator.js +153 -0
- package/dist/watcher/file-watcher.d.ts +27 -0
- package/dist/watcher/file-watcher.d.ts.map +1 -0
- package/dist/watcher/file-watcher.js +110 -0
- package/{src/watcher/index.ts → dist/watcher/index.d.ts} +1 -1
- package/dist/watcher/index.d.ts.map +1 -0
- package/dist/watcher/index.js +10 -0
- package/package.json +12 -9
- package/src/generator/index.ts +0 -13
- package/src/generator/route-generator.test.ts +0 -287
- package/src/generator/route-generator.ts +0 -231
- package/src/index.ts +0 -158
- package/src/scanner/file-scanner.test.ts +0 -271
- package/src/scanner/file-scanner.ts +0 -329
- package/src/scanner/index.ts +0 -15
- package/src/validator/route-validator.test.ts +0 -192
- package/src/validator/route-validator.ts +0 -178
- package/src/watcher/file-watcher.ts +0 -132
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { buildUrlPath, extractParams, filePathToScreenName, type RouteNode, scanRoutesDirectory } from "./file-scanner";
|
|
5
|
-
|
|
6
|
-
const TEST_DIR = join(import.meta.dir, "__test_routes__");
|
|
7
|
-
|
|
8
|
-
function createTestFile(relativePath: string, content = "export default {}") {
|
|
9
|
-
const fullPath = join(TEST_DIR, relativePath);
|
|
10
|
-
const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
|
|
11
|
-
mkdirSync(dir, { recursive: true });
|
|
12
|
-
writeFileSync(fullPath, content);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe("File Scanner", () => {
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
mkdirSync(TEST_DIR, { recursive: true });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe("extractParams", () => {
|
|
25
|
-
it("should extract required param from [param]", () => {
|
|
26
|
-
const params = extractParams("[userId]");
|
|
27
|
-
expect(params).toEqual([{ name: "userId", isOptional: false, isCatchAll: false }]);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("should extract optional param from [param]?", () => {
|
|
31
|
-
const params = extractParams("[section]?");
|
|
32
|
-
expect(params).toEqual([{ name: "section", isOptional: true, isCatchAll: false }]);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("should extract catch-all param from [...slug]", () => {
|
|
36
|
-
const params = extractParams("[...slug]");
|
|
37
|
-
expect(params).toEqual([{ name: "slug", isOptional: false, isCatchAll: true }]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("should return empty array for static filename", () => {
|
|
41
|
-
const params = extractParams("about");
|
|
42
|
-
expect(params).toEqual([]);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("should return empty array for index", () => {
|
|
46
|
-
const params = extractParams("index");
|
|
47
|
-
expect(params).toEqual([]);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("should return empty array for _layout", () => {
|
|
51
|
-
const params = extractParams("_layout");
|
|
52
|
-
expect(params).toEqual([]);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("buildUrlPath", () => {
|
|
57
|
-
it("should build root path for index.tsx", () => {
|
|
58
|
-
const path = buildUrlPath("index.tsx", true, false);
|
|
59
|
-
expect(path).toBe("/");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("should build static path", () => {
|
|
63
|
-
const path = buildUrlPath("about.tsx", false, false);
|
|
64
|
-
expect(path).toBe("/about");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("should build nested path", () => {
|
|
68
|
-
const path = buildUrlPath("users/index.tsx", true, false);
|
|
69
|
-
expect(path).toBe("/users");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("should build dynamic path with required param", () => {
|
|
73
|
-
const path = buildUrlPath("users/[userId].tsx", false, false);
|
|
74
|
-
expect(path).toBe("/users/:userId");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("should build dynamic path with optional param", () => {
|
|
78
|
-
const path = buildUrlPath("settings/[section]?.tsx", false, false);
|
|
79
|
-
expect(path).toBe("/settings/:section?");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("should build catch-all path", () => {
|
|
83
|
-
const path = buildUrlPath("docs/[...slug].tsx", false, false);
|
|
84
|
-
expect(path).toBe("/docs/*");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("should remove route groups from path", () => {
|
|
88
|
-
const path = buildUrlPath("(tabs)/home.tsx", false, false);
|
|
89
|
-
expect(path).toBe("/home");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("should return empty string for layout files", () => {
|
|
93
|
-
const path = buildUrlPath("_layout.tsx", false, true);
|
|
94
|
-
expect(path).toBe("");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("should handle deeply nested dynamic paths", () => {
|
|
98
|
-
const path = buildUrlPath("users/[userId]/posts/[postId].tsx", false, false);
|
|
99
|
-
expect(path).toBe("/users/:userId/posts/:postId");
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe("filePathToScreenName", () => {
|
|
104
|
-
it("should convert file path to screen name", () => {
|
|
105
|
-
expect(filePathToScreenName("users/[userId].tsx")).toBe("users/[userId]");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("should handle index files", () => {
|
|
109
|
-
expect(filePathToScreenName("users/index.tsx")).toBe("users/index");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("should handle layout files", () => {
|
|
113
|
-
expect(filePathToScreenName("users/_layout.tsx")).toBe("users/_layout");
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe("scanRoutesDirectory", () => {
|
|
118
|
-
it("should return empty routes for non-existent directory", () => {
|
|
119
|
-
const result = scanRoutesDirectory("/non/existent/path");
|
|
120
|
-
expect(result.routes).toEqual([]);
|
|
121
|
-
expect(result.errors.length).toBeGreaterThan(0);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("should scan single index file", () => {
|
|
125
|
-
createTestFile("index.tsx", "export default function Home() { return null; }");
|
|
126
|
-
|
|
127
|
-
const result = scanRoutesDirectory(TEST_DIR);
|
|
128
|
-
expect(result.errors).toEqual([]);
|
|
129
|
-
expect(result.routes.length).toBe(1);
|
|
130
|
-
expect(result.routes[0].path).toBe("/");
|
|
131
|
-
expect(result.routes[0].isIndex).toBe(true);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("should scan static route file", () => {
|
|
135
|
-
createTestFile("about.tsx", "export default function About() { return null; }");
|
|
136
|
-
|
|
137
|
-
const result = scanRoutesDirectory(TEST_DIR);
|
|
138
|
-
expect(result.routes.length).toBe(1);
|
|
139
|
-
expect(result.routes[0].path).toBe("/about");
|
|
140
|
-
expect(result.routes[0].isIndex).toBe(false);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("should scan dynamic route file", () => {
|
|
144
|
-
createTestFile("users/[userId].tsx", "export default function User() { return null; }");
|
|
145
|
-
|
|
146
|
-
const result = scanRoutesDirectory(TEST_DIR);
|
|
147
|
-
const userRoute = result.routes.find((r) => r.path === "/users/:userId");
|
|
148
|
-
expect(userRoute).toBeDefined();
|
|
149
|
-
expect(userRoute?.params).toEqual([{ name: "userId", isOptional: false, isCatchAll: false }]);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("should scan layout files", () => {
|
|
153
|
-
createTestFile("_layout.tsx", "export default { type: 'stack' }");
|
|
154
|
-
|
|
155
|
-
const result = scanRoutesDirectory(TEST_DIR);
|
|
156
|
-
expect(result.routes.length).toBe(1);
|
|
157
|
-
expect(result.routes[0].isLayout).toBe(true);
|
|
158
|
-
expect(result.routes[0].layoutType).toBe("stack");
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("should scan tab layout files", () => {
|
|
162
|
-
createTestFile(
|
|
163
|
-
"(tabs)/_layout.tsx",
|
|
164
|
-
`
|
|
165
|
-
import { defineLayout } from '@teardown/navigation';
|
|
166
|
-
export default defineLayout({ type: 'tabs' });
|
|
167
|
-
`
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
const result = scanRoutesDirectory(TEST_DIR);
|
|
171
|
-
const tabLayout = result.routes.find((r) => r.isLayout && r.groupName === "tabs");
|
|
172
|
-
expect(tabLayout).toBeDefined();
|
|
173
|
-
expect(tabLayout?.layoutType).toBe("tabs");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("should detect route groups", () => {
|
|
177
|
-
createTestFile("(auth)/login.tsx", "export default function Login() { return null; }");
|
|
178
|
-
|
|
179
|
-
const result = scanRoutesDirectory(TEST_DIR);
|
|
180
|
-
const loginRoute = result.routes.find((r) => r.path === "/login");
|
|
181
|
-
expect(loginRoute).toBeDefined();
|
|
182
|
-
expect(loginRoute?.groupName).toBe("auth");
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it("should ignore hidden files starting with underscore (except _layout)", () => {
|
|
186
|
-
createTestFile("_hidden.tsx", "export default {}");
|
|
187
|
-
createTestFile("about.tsx", "export default {}");
|
|
188
|
-
|
|
189
|
-
const result = scanRoutesDirectory(TEST_DIR);
|
|
190
|
-
expect(result.routes.length).toBe(1);
|
|
191
|
-
expect(result.routes[0].path).toBe("/about");
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("should ignore test files", () => {
|
|
195
|
-
createTestFile("about.test.tsx", "export default {}");
|
|
196
|
-
createTestFile("about.tsx", "export default {}");
|
|
197
|
-
|
|
198
|
-
const result = scanRoutesDirectory(TEST_DIR);
|
|
199
|
-
expect(result.routes.length).toBe(1);
|
|
200
|
-
expect(result.routes[0].path).toBe("/about");
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("should scan catch-all routes", () => {
|
|
204
|
-
createTestFile("[...catchAll].tsx", "export default function CatchAll() { return null; }");
|
|
205
|
-
|
|
206
|
-
const result = scanRoutesDirectory(TEST_DIR);
|
|
207
|
-
expect(result.routes.length).toBe(1);
|
|
208
|
-
expect(result.routes[0].path).toBe("/*");
|
|
209
|
-
expect(result.routes[0].isCatchAll).toBe(true);
|
|
210
|
-
expect(result.routes[0].params).toEqual([{ name: "catchAll", isOptional: false, isCatchAll: true }]);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("should build route tree with parent-child relationships", () => {
|
|
214
|
-
createTestFile("users/_layout.tsx", "export default { type: 'stack' }");
|
|
215
|
-
createTestFile("users/index.tsx", "export default {}");
|
|
216
|
-
createTestFile("users/[userId].tsx", "export default {}");
|
|
217
|
-
|
|
218
|
-
const result = scanRoutesDirectory(TEST_DIR);
|
|
219
|
-
|
|
220
|
-
// Find the layout
|
|
221
|
-
const layout = result.routes.find((r) => r.isLayout && r.relativePath.includes("users"));
|
|
222
|
-
expect(layout).toBeDefined();
|
|
223
|
-
expect(layout?.children.length).toBe(2);
|
|
224
|
-
|
|
225
|
-
// Children should be index and [userId]
|
|
226
|
-
const childPaths = layout?.children.map((c) => c.path) || [];
|
|
227
|
-
expect(childPaths).toContain("/users");
|
|
228
|
-
expect(childPaths).toContain("/users/:userId");
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it("should handle complex nested structure", () => {
|
|
232
|
-
createTestFile("_layout.tsx", "export default { type: 'stack' }");
|
|
233
|
-
createTestFile("index.tsx", "export default {}");
|
|
234
|
-
createTestFile("about.tsx", "export default {}");
|
|
235
|
-
createTestFile("users/_layout.tsx", "export default { type: 'stack' }");
|
|
236
|
-
createTestFile("users/index.tsx", "export default {}");
|
|
237
|
-
createTestFile("users/[userId].tsx", "export default {}");
|
|
238
|
-
createTestFile("users/[userId]/posts/[postId].tsx", "export default {}");
|
|
239
|
-
createTestFile("(tabs)/_layout.tsx", "export default { type: 'tabs' }");
|
|
240
|
-
createTestFile("(tabs)/home.tsx", "export default {}");
|
|
241
|
-
createTestFile("(tabs)/settings.tsx", "export default {}");
|
|
242
|
-
|
|
243
|
-
const result = scanRoutesDirectory(TEST_DIR);
|
|
244
|
-
|
|
245
|
-
// Should have root layout with children
|
|
246
|
-
const rootLayout = result.routes.find((r) => r.isLayout && r.relativePath === "_layout.tsx");
|
|
247
|
-
expect(rootLayout).toBeDefined();
|
|
248
|
-
|
|
249
|
-
// Flatten all routes for easier checking
|
|
250
|
-
const allRoutes = flattenRoutes(result.routes);
|
|
251
|
-
const allPaths = allRoutes.filter((r) => !r.isLayout).map((r) => r.path);
|
|
252
|
-
|
|
253
|
-
expect(allPaths).toContain("/");
|
|
254
|
-
expect(allPaths).toContain("/about");
|
|
255
|
-
expect(allPaths).toContain("/users");
|
|
256
|
-
expect(allPaths).toContain("/users/:userId");
|
|
257
|
-
expect(allPaths).toContain("/users/:userId/posts/:postId");
|
|
258
|
-
expect(allPaths).toContain("/home");
|
|
259
|
-
expect(allPaths).toContain("/settings");
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
function flattenRoutes(routes: RouteNode[]): RouteNode[] {
|
|
265
|
-
const result: RouteNode[] = [];
|
|
266
|
-
for (const route of routes) {
|
|
267
|
-
result.push(route);
|
|
268
|
-
result.push(...flattenRoutes(route.children));
|
|
269
|
-
}
|
|
270
|
-
return result;
|
|
271
|
-
}
|
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File scanner for @teardown/navigation-metro
|
|
3
|
-
* Scans routes directory and builds a route tree for type generation
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
7
|
-
import { basename, dirname, extname, join } from "node:path";
|
|
8
|
-
|
|
9
|
-
export interface RouteNode {
|
|
10
|
-
/** Screen name derived from file path */
|
|
11
|
-
name: string;
|
|
12
|
-
/** URL path for this route */
|
|
13
|
-
path: string;
|
|
14
|
-
/** Absolute file path */
|
|
15
|
-
filePath: string;
|
|
16
|
-
/** Relative file path from routes dir */
|
|
17
|
-
relativePath: string;
|
|
18
|
-
/** Extracted dynamic params */
|
|
19
|
-
params: ParamDefinition[];
|
|
20
|
-
/** Child routes */
|
|
21
|
-
children: RouteNode[];
|
|
22
|
-
/** Navigator type from _layout.tsx */
|
|
23
|
-
layoutType: "stack" | "tabs" | "drawer" | "none";
|
|
24
|
-
/** Is this an index route */
|
|
25
|
-
isIndex: boolean;
|
|
26
|
-
/** Is this a layout file */
|
|
27
|
-
isLayout: boolean;
|
|
28
|
-
/** Is this a catch-all route */
|
|
29
|
-
isCatchAll: boolean;
|
|
30
|
-
/** Route group name (from parentheses) */
|
|
31
|
-
groupName: string | null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface ParamDefinition {
|
|
35
|
-
name: string;
|
|
36
|
-
isOptional: boolean;
|
|
37
|
-
isCatchAll: boolean;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface ScanResult {
|
|
41
|
-
routes: RouteNode[];
|
|
42
|
-
errors: ScanError[];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface ScanError {
|
|
46
|
-
file: string;
|
|
47
|
-
message: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Scans a routes directory and builds a route tree
|
|
52
|
-
*/
|
|
53
|
-
export function scanRoutesDirectory(routesDir: string): ScanResult {
|
|
54
|
-
const errors: ScanError[] = [];
|
|
55
|
-
|
|
56
|
-
if (!existsSync(routesDir)) {
|
|
57
|
-
return {
|
|
58
|
-
routes: [],
|
|
59
|
-
errors: [{ file: routesDir, message: "Routes directory does not exist" }],
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const files = findRouteFiles(routesDir);
|
|
64
|
-
const routeNodes = new Map<string, RouteNode>();
|
|
65
|
-
|
|
66
|
-
// First pass: create all route nodes
|
|
67
|
-
for (const file of files) {
|
|
68
|
-
const absolutePath = join(routesDir, file);
|
|
69
|
-
const node = parseRouteFile(file, absolutePath);
|
|
70
|
-
|
|
71
|
-
if (node) {
|
|
72
|
-
routeNodes.set(file, node);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Second pass: build tree structure
|
|
77
|
-
const rootNodes: RouteNode[] = [];
|
|
78
|
-
|
|
79
|
-
for (const [filePath, node] of routeNodes) {
|
|
80
|
-
const parentPath = findParentLayoutPath(filePath, routeNodes);
|
|
81
|
-
|
|
82
|
-
if (parentPath) {
|
|
83
|
-
const parent = routeNodes.get(parentPath);
|
|
84
|
-
if (parent) {
|
|
85
|
-
parent.children.push(node);
|
|
86
|
-
}
|
|
87
|
-
} else {
|
|
88
|
-
rootNodes.push(node);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return { routes: rootNodes, errors };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Recursively finds all route files in a directory
|
|
97
|
-
*/
|
|
98
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: route file discovery branches
|
|
99
|
-
function findRouteFiles(dir: string, prefix = ""): string[] {
|
|
100
|
-
const results: string[] = [];
|
|
101
|
-
|
|
102
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
103
|
-
|
|
104
|
-
for (const entry of entries) {
|
|
105
|
-
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
106
|
-
|
|
107
|
-
if (entry.isDirectory()) {
|
|
108
|
-
// Skip node_modules and hidden directories (except those in parentheses for groups)
|
|
109
|
-
if (entry.name === "node_modules" || (entry.name.startsWith(".") && !entry.name.startsWith("("))) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
results.push(...findRouteFiles(join(dir, entry.name), relativePath));
|
|
113
|
-
} else if (entry.isFile()) {
|
|
114
|
-
// Only include .ts and .tsx files
|
|
115
|
-
const ext = extname(entry.name);
|
|
116
|
-
if (ext !== ".ts" && ext !== ".tsx") {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Skip test files
|
|
121
|
-
if (entry.name.includes(".test.") || entry.name.includes(".spec.")) {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Skip hidden files (starting with _) except _layout
|
|
126
|
-
const baseName = basename(entry.name, ext);
|
|
127
|
-
if (baseName.startsWith("_") && baseName !== "_layout") {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
results.push(relativePath);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return results;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Parses a route file and creates a RouteNode
|
|
140
|
-
*/
|
|
141
|
-
function parseRouteFile(relativePath: string, absolutePath: string): RouteNode | null {
|
|
142
|
-
const ext = extname(relativePath);
|
|
143
|
-
const fileName = basename(relativePath, ext);
|
|
144
|
-
|
|
145
|
-
const isLayout = fileName === "_layout";
|
|
146
|
-
const isIndex = fileName === "index";
|
|
147
|
-
const isCatchAll = fileName.startsWith("[...");
|
|
148
|
-
|
|
149
|
-
// Extract route group from path
|
|
150
|
-
const groupMatch = relativePath.match(/\(([^)]+)\)/);
|
|
151
|
-
const groupName = groupMatch ? groupMatch[1] : null;
|
|
152
|
-
|
|
153
|
-
// Parse params from the entire relative path (not just filename)
|
|
154
|
-
// This extracts params from both directory names and the filename
|
|
155
|
-
const params = extractParams(relativePath);
|
|
156
|
-
|
|
157
|
-
// Build URL path
|
|
158
|
-
const urlPath = buildUrlPath(relativePath, isIndex, isLayout);
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
name: filePathToScreenName(relativePath),
|
|
162
|
-
path: urlPath,
|
|
163
|
-
filePath: absolutePath,
|
|
164
|
-
relativePath,
|
|
165
|
-
params,
|
|
166
|
-
children: [],
|
|
167
|
-
layoutType: isLayout ? detectLayoutType(absolutePath) : "none",
|
|
168
|
-
isIndex,
|
|
169
|
-
isLayout,
|
|
170
|
-
isCatchAll,
|
|
171
|
-
groupName,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Extracts dynamic parameters from a filename
|
|
177
|
-
*/
|
|
178
|
-
export function extractParams(fileName: string): ParamDefinition[] {
|
|
179
|
-
const params: ParamDefinition[] = [];
|
|
180
|
-
const regex = /\[(?:\.\.\.)?([^\]]+)\]\??/g;
|
|
181
|
-
let match: RegExpExecArray | null;
|
|
182
|
-
|
|
183
|
-
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex iteration pattern
|
|
184
|
-
while ((match = regex.exec(fileName)) !== null) {
|
|
185
|
-
const fullMatch = match[0];
|
|
186
|
-
const paramName = match[1].replace("?", "");
|
|
187
|
-
|
|
188
|
-
params.push({
|
|
189
|
-
name: paramName,
|
|
190
|
-
isOptional: fullMatch.endsWith("?"),
|
|
191
|
-
isCatchAll: fullMatch.startsWith("[..."),
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return params;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Builds a URL path from a relative file path
|
|
200
|
-
*/
|
|
201
|
-
export function buildUrlPath(relativePath: string, _isIndex: boolean, isLayout: boolean): string {
|
|
202
|
-
if (isLayout) return "";
|
|
203
|
-
|
|
204
|
-
const ext = extname(relativePath);
|
|
205
|
-
let urlPath = relativePath
|
|
206
|
-
.replace(ext, "") // Remove extension
|
|
207
|
-
.replace(/\\/g, "/") // Normalize path separators
|
|
208
|
-
.replace(/\(([^)]+)\)\//g, "") // Remove route groups from URL
|
|
209
|
-
.replace(/\[\.\.\.([^\]]+)\]/g, "*") // Catch-all
|
|
210
|
-
.replace(/\[([^\]]+)\]\?/g, ":$1?") // Optional params
|
|
211
|
-
.replace(/\[([^\]]+)\]/g, ":$1"); // Required params
|
|
212
|
-
|
|
213
|
-
// Handle index files - remove "index" or trailing "/index"
|
|
214
|
-
if (urlPath === "index") {
|
|
215
|
-
return "/";
|
|
216
|
-
}
|
|
217
|
-
urlPath = urlPath.replace(/\/index$/, "");
|
|
218
|
-
|
|
219
|
-
return `/${urlPath}` || "/";
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Converts a file path to a screen name
|
|
224
|
-
*/
|
|
225
|
-
export function filePathToScreenName(relativePath: string): string {
|
|
226
|
-
const ext = extname(relativePath);
|
|
227
|
-
return relativePath.replace(ext, "").replace(/\\/g, "/");
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Detects the layout type from a _layout.tsx file
|
|
232
|
-
*/
|
|
233
|
-
function detectLayoutType(absolutePath: string): "stack" | "tabs" | "drawer" {
|
|
234
|
-
try {
|
|
235
|
-
const content = readFileSync(absolutePath, "utf-8");
|
|
236
|
-
|
|
237
|
-
if (content.includes("type: 'tabs'") || content.includes('type: "tabs"')) {
|
|
238
|
-
return "tabs";
|
|
239
|
-
}
|
|
240
|
-
if (content.includes("type: 'drawer'") || content.includes('type: "drawer"')) {
|
|
241
|
-
return "drawer";
|
|
242
|
-
}
|
|
243
|
-
} catch {
|
|
244
|
-
// Default to stack on read error
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return "stack";
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Finds the parent layout file path for a given route file
|
|
252
|
-
*/
|
|
253
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: layout path traversal
|
|
254
|
-
function findParentLayoutPath(filePath: string, routeNodes: Map<string, RouteNode>): string | null {
|
|
255
|
-
const dir = dirname(filePath);
|
|
256
|
-
const fileName = basename(filePath);
|
|
257
|
-
const isLayout = fileName === "_layout.tsx" || fileName === "_layout.ts";
|
|
258
|
-
|
|
259
|
-
// For root level files
|
|
260
|
-
if (dir === ".") {
|
|
261
|
-
// Check if there's a root _layout.tsx and this file is not the layout itself
|
|
262
|
-
const rootLayout = "_layout.tsx";
|
|
263
|
-
if (filePath !== rootLayout && routeNodes.has(rootLayout)) {
|
|
264
|
-
return rootLayout;
|
|
265
|
-
}
|
|
266
|
-
const rootLayoutTs = "_layout.ts";
|
|
267
|
-
if (filePath !== rootLayoutTs && routeNodes.has(rootLayoutTs)) {
|
|
268
|
-
return rootLayoutTs;
|
|
269
|
-
}
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// For non-layout files, look for _layout in the same directory first
|
|
274
|
-
if (!isLayout) {
|
|
275
|
-
const sameDirLayout = `${dir}/_layout.tsx`;
|
|
276
|
-
const sameDirLayoutTs = `${dir}/_layout.ts`;
|
|
277
|
-
|
|
278
|
-
if (routeNodes.has(sameDirLayout)) {
|
|
279
|
-
return sameDirLayout;
|
|
280
|
-
}
|
|
281
|
-
if (routeNodes.has(sameDirLayoutTs)) {
|
|
282
|
-
return sameDirLayoutTs;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// For layouts, or if no layout in same directory, look in parent directories
|
|
287
|
-
let parentDir = dirname(dir);
|
|
288
|
-
|
|
289
|
-
while (parentDir !== ".") {
|
|
290
|
-
const layoutPath = `${parentDir}/_layout.tsx`;
|
|
291
|
-
const layoutPathTs = `${parentDir}/_layout.ts`;
|
|
292
|
-
|
|
293
|
-
if (routeNodes.has(layoutPath)) {
|
|
294
|
-
return layoutPath;
|
|
295
|
-
}
|
|
296
|
-
if (routeNodes.has(layoutPathTs)) {
|
|
297
|
-
return layoutPathTs;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
parentDir = dirname(parentDir);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Check root level layout
|
|
304
|
-
if (routeNodes.has("_layout.tsx") && filePath !== "_layout.tsx") {
|
|
305
|
-
return "_layout.tsx";
|
|
306
|
-
}
|
|
307
|
-
if (routeNodes.has("_layout.ts") && filePath !== "_layout.ts") {
|
|
308
|
-
return "_layout.ts";
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Flattens a route tree into a flat array
|
|
316
|
-
*/
|
|
317
|
-
export function flattenRoutes(routes: RouteNode[]): RouteNode[] {
|
|
318
|
-
const result: RouteNode[] = [];
|
|
319
|
-
|
|
320
|
-
function traverse(nodes: RouteNode[]): void {
|
|
321
|
-
for (const node of nodes) {
|
|
322
|
-
result.push(node);
|
|
323
|
-
traverse(node.children);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
traverse(routes);
|
|
328
|
-
return result;
|
|
329
|
-
}
|
package/src/scanner/index.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scanner module for @teardown/navigation-metro
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export {
|
|
6
|
-
buildUrlPath,
|
|
7
|
-
extractParams,
|
|
8
|
-
filePathToScreenName,
|
|
9
|
-
flattenRoutes,
|
|
10
|
-
type ParamDefinition,
|
|
11
|
-
type RouteNode,
|
|
12
|
-
type ScanError,
|
|
13
|
-
type ScanResult,
|
|
14
|
-
scanRoutesDirectory,
|
|
15
|
-
} from "./file-scanner";
|