@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
package/src/generator/index.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generator module for @teardown/navigation-metro
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export {
|
|
6
|
-
buildRouteParamsInterface,
|
|
7
|
-
type GenerateOptions,
|
|
8
|
-
generateAllRouteFiles,
|
|
9
|
-
generateLinkingFileContent,
|
|
10
|
-
generateRegisterFileContent,
|
|
11
|
-
generateRoutesFileContent,
|
|
12
|
-
type RouteParamEntry,
|
|
13
|
-
} from "./route-generator";
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { scanRoutesDirectory } from "../scanner/file-scanner";
|
|
5
|
-
import {
|
|
6
|
-
buildRouteParamsInterface,
|
|
7
|
-
generateAllRouteFiles,
|
|
8
|
-
generateLinkingFileContent,
|
|
9
|
-
generateRegisterFileContent,
|
|
10
|
-
generateRoutesFileContent,
|
|
11
|
-
} from "./route-generator";
|
|
12
|
-
|
|
13
|
-
const TEST_ROUTES_DIR = join(import.meta.dir, "__test_routes__");
|
|
14
|
-
const TEST_GENERATED_DIR = join(import.meta.dir, "__test_generated__");
|
|
15
|
-
|
|
16
|
-
function createTestFile(relativePath: string, content = "export default {}") {
|
|
17
|
-
const fullPath = join(TEST_ROUTES_DIR, relativePath);
|
|
18
|
-
const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
|
|
19
|
-
mkdirSync(dir, { recursive: true });
|
|
20
|
-
writeFileSync(fullPath, content);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe("Route Generator", () => {
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
mkdirSync(TEST_ROUTES_DIR, { recursive: true });
|
|
26
|
-
mkdirSync(TEST_GENERATED_DIR, { recursive: true });
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
rmSync(TEST_ROUTES_DIR, { recursive: true, force: true });
|
|
31
|
-
rmSync(TEST_GENERATED_DIR, { recursive: true, force: true });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe("buildRouteParamsInterface", () => {
|
|
35
|
-
it("should return empty array for empty routes", () => {
|
|
36
|
-
const result = buildRouteParamsInterface([]);
|
|
37
|
-
expect(result).toEqual([]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("should generate static route with undefined params", () => {
|
|
41
|
-
createTestFile("about.tsx");
|
|
42
|
-
const { routes } = scanRoutesDirectory(TEST_ROUTES_DIR);
|
|
43
|
-
|
|
44
|
-
const result = buildRouteParamsInterface(routes);
|
|
45
|
-
expect(result).toEqual([{ path: "/about", params: [] }]);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("should generate dynamic route with params", () => {
|
|
49
|
-
createTestFile("users/[userId].tsx");
|
|
50
|
-
const { routes } = scanRoutesDirectory(TEST_ROUTES_DIR);
|
|
51
|
-
|
|
52
|
-
const result = buildRouteParamsInterface(routes);
|
|
53
|
-
const userRoute = result.find((r) => r.path === "/users/:userId");
|
|
54
|
-
expect(userRoute).toBeDefined();
|
|
55
|
-
expect(userRoute?.params).toEqual([{ name: "userId", isOptional: false, isCatchAll: false }]);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("should handle optional params", () => {
|
|
59
|
-
createTestFile("settings/[section]?.tsx");
|
|
60
|
-
const { routes } = scanRoutesDirectory(TEST_ROUTES_DIR);
|
|
61
|
-
|
|
62
|
-
const result = buildRouteParamsInterface(routes);
|
|
63
|
-
const settingsRoute = result.find((r) => r.path === "/settings/:section?");
|
|
64
|
-
expect(settingsRoute).toBeDefined();
|
|
65
|
-
expect(settingsRoute?.params).toEqual([{ name: "section", isOptional: true, isCatchAll: false }]);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("should handle catch-all params", () => {
|
|
69
|
-
createTestFile("[...slug].tsx");
|
|
70
|
-
const { routes } = scanRoutesDirectory(TEST_ROUTES_DIR);
|
|
71
|
-
|
|
72
|
-
const result = buildRouteParamsInterface(routes);
|
|
73
|
-
const catchAllRoute = result.find((r) => r.path === "/*");
|
|
74
|
-
expect(catchAllRoute).toBeDefined();
|
|
75
|
-
expect(catchAllRoute?.params).toEqual([{ name: "slug", isOptional: false, isCatchAll: true }]);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("should accumulate params from parent layouts", () => {
|
|
79
|
-
createTestFile("users/_layout.tsx");
|
|
80
|
-
createTestFile("users/[userId]/_layout.tsx");
|
|
81
|
-
createTestFile("users/[userId]/posts/[postId].tsx");
|
|
82
|
-
const { routes } = scanRoutesDirectory(TEST_ROUTES_DIR);
|
|
83
|
-
|
|
84
|
-
const result = buildRouteParamsInterface(routes);
|
|
85
|
-
const postRoute = result.find((r) => r.path === "/users/:userId/posts/:postId");
|
|
86
|
-
expect(postRoute).toBeDefined();
|
|
87
|
-
expect(postRoute?.params.length).toBe(2);
|
|
88
|
-
expect(postRoute?.params.map((p) => p.name)).toContain("userId");
|
|
89
|
-
expect(postRoute?.params.map((p) => p.name)).toContain("postId");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("should skip layout files from route entries", () => {
|
|
93
|
-
createTestFile("_layout.tsx");
|
|
94
|
-
createTestFile("index.tsx");
|
|
95
|
-
const { routes } = scanRoutesDirectory(TEST_ROUTES_DIR);
|
|
96
|
-
|
|
97
|
-
const result = buildRouteParamsInterface(routes);
|
|
98
|
-
// Should only have index, not layout
|
|
99
|
-
expect(result.length).toBe(1);
|
|
100
|
-
expect(result[0].path).toBe("/");
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe("generateRoutesFileContent", () => {
|
|
105
|
-
it("should generate valid TypeScript for static routes", () => {
|
|
106
|
-
createTestFile("index.tsx");
|
|
107
|
-
createTestFile("about.tsx");
|
|
108
|
-
const { routes } = scanRoutesDirectory(TEST_ROUTES_DIR);
|
|
109
|
-
|
|
110
|
-
const content = generateRoutesFileContent(routes);
|
|
111
|
-
|
|
112
|
-
expect(content).toContain("export interface RouteParams");
|
|
113
|
-
expect(content).toContain('"/": undefined');
|
|
114
|
-
expect(content).toContain('"/about": undefined');
|
|
115
|
-
expect(content).toContain("export type RoutePath = keyof RouteParams");
|
|
116
|
-
expect(content).toContain("export type ParamsFor<T extends RoutePath> = RouteParams[T]");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("should generate valid TypeScript for dynamic routes", () => {
|
|
120
|
-
createTestFile("users/[userId].tsx");
|
|
121
|
-
const { routes } = scanRoutesDirectory(TEST_ROUTES_DIR);
|
|
122
|
-
|
|
123
|
-
const content = generateRoutesFileContent(routes);
|
|
124
|
-
|
|
125
|
-
expect(content).toContain('"/users/:userId": { userId: string }');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("should generate valid TypeScript for optional params", () => {
|
|
129
|
-
createTestFile("settings/[section]?.tsx");
|
|
130
|
-
const { routes } = scanRoutesDirectory(TEST_ROUTES_DIR);
|
|
131
|
-
|
|
132
|
-
const content = generateRoutesFileContent(routes);
|
|
133
|
-
|
|
134
|
-
expect(content).toContain('"/settings/:section?": { section?: string }');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("should generate valid TypeScript for catch-all routes", () => {
|
|
138
|
-
createTestFile("[...slug].tsx");
|
|
139
|
-
const { routes } = scanRoutesDirectory(TEST_ROUTES_DIR);
|
|
140
|
-
|
|
141
|
-
const content = generateRoutesFileContent(routes);
|
|
142
|
-
|
|
143
|
-
expect(content).toContain('"/*": { slug: string[] }');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("should include RouteWithParams utility type", () => {
|
|
147
|
-
createTestFile("index.tsx");
|
|
148
|
-
const { routes } = scanRoutesDirectory(TEST_ROUTES_DIR);
|
|
149
|
-
|
|
150
|
-
const content = generateRoutesFileContent(routes);
|
|
151
|
-
|
|
152
|
-
expect(content).toContain("export type RouteWithParams");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("should include NavigatorType", () => {
|
|
156
|
-
createTestFile("index.tsx");
|
|
157
|
-
const { routes } = scanRoutesDirectory(TEST_ROUTES_DIR);
|
|
158
|
-
|
|
159
|
-
const content = generateRoutesFileContent(routes);
|
|
160
|
-
|
|
161
|
-
expect(content).toContain('export type NavigatorType = "stack" | "tabs" | "drawer"');
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe("generateLinkingFileContent", () => {
|
|
166
|
-
it("should generate linking config for static routes", () => {
|
|
167
|
-
createTestFile("index.tsx");
|
|
168
|
-
createTestFile("about.tsx");
|
|
169
|
-
const { routes } = scanRoutesDirectory(TEST_ROUTES_DIR);
|
|
170
|
-
|
|
171
|
-
const content = generateLinkingFileContent(routes, ["myapp://"]);
|
|
172
|
-
|
|
173
|
-
expect(content).toContain("export const generatedLinkingConfig");
|
|
174
|
-
expect(content).toContain('"myapp://"');
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("should convert dynamic params to react-navigation format", () => {
|
|
178
|
-
createTestFile("users/[userId].tsx");
|
|
179
|
-
const { routes } = scanRoutesDirectory(TEST_ROUTES_DIR);
|
|
180
|
-
|
|
181
|
-
const content = generateLinkingFileContent(routes, []);
|
|
182
|
-
|
|
183
|
-
// Should have :userId format for react-navigation
|
|
184
|
-
expect(content).toContain('"/users/:userId"');
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
describe("generateRegisterFileContent", () => {
|
|
189
|
-
it("should generate module augmentation", () => {
|
|
190
|
-
const content = generateRegisterFileContent();
|
|
191
|
-
|
|
192
|
-
expect(content).toContain("declare module '@teardown/navigation'");
|
|
193
|
-
expect(content).toContain("interface Register");
|
|
194
|
-
expect(content).toContain("routeParams: RouteParams");
|
|
195
|
-
expect(content).toContain("routePath: RoutePath");
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
describe("generateAllRouteFiles", () => {
|
|
200
|
-
it("should create generated directory if not exists", () => {
|
|
201
|
-
rmSync(TEST_GENERATED_DIR, { recursive: true, force: true });
|
|
202
|
-
createTestFile("index.tsx");
|
|
203
|
-
|
|
204
|
-
generateAllRouteFiles({
|
|
205
|
-
routesDir: TEST_ROUTES_DIR,
|
|
206
|
-
generatedDir: TEST_GENERATED_DIR,
|
|
207
|
-
prefixes: [],
|
|
208
|
-
verbose: false,
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
expect(existsSync(TEST_GENERATED_DIR)).toBe(true);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("should generate routes.generated.ts", () => {
|
|
215
|
-
createTestFile("index.tsx");
|
|
216
|
-
createTestFile("about.tsx");
|
|
217
|
-
|
|
218
|
-
generateAllRouteFiles({
|
|
219
|
-
routesDir: TEST_ROUTES_DIR,
|
|
220
|
-
generatedDir: TEST_GENERATED_DIR,
|
|
221
|
-
prefixes: [],
|
|
222
|
-
verbose: false,
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
const generatedPath = join(TEST_GENERATED_DIR, "routes.generated.ts");
|
|
226
|
-
expect(existsSync(generatedPath)).toBe(true);
|
|
227
|
-
|
|
228
|
-
const content = readFileSync(generatedPath, "utf-8");
|
|
229
|
-
expect(content).toContain("RouteParams");
|
|
230
|
-
expect(content).toContain('"/about"');
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it("should generate linking.generated.ts", () => {
|
|
234
|
-
createTestFile("index.tsx");
|
|
235
|
-
|
|
236
|
-
generateAllRouteFiles({
|
|
237
|
-
routesDir: TEST_ROUTES_DIR,
|
|
238
|
-
generatedDir: TEST_GENERATED_DIR,
|
|
239
|
-
prefixes: ["myapp://"],
|
|
240
|
-
verbose: false,
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
const generatedPath = join(TEST_GENERATED_DIR, "linking.generated.ts");
|
|
244
|
-
expect(existsSync(generatedPath)).toBe(true);
|
|
245
|
-
|
|
246
|
-
const content = readFileSync(generatedPath, "utf-8");
|
|
247
|
-
expect(content).toContain("generatedLinkingConfig");
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("should generate register.d.ts", () => {
|
|
251
|
-
createTestFile("index.tsx");
|
|
252
|
-
|
|
253
|
-
generateAllRouteFiles({
|
|
254
|
-
routesDir: TEST_ROUTES_DIR,
|
|
255
|
-
generatedDir: TEST_GENERATED_DIR,
|
|
256
|
-
prefixes: [],
|
|
257
|
-
verbose: false,
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
const generatedPath = join(TEST_GENERATED_DIR, "register.d.ts");
|
|
261
|
-
expect(existsSync(generatedPath)).toBe(true);
|
|
262
|
-
|
|
263
|
-
const content = readFileSync(generatedPath, "utf-8");
|
|
264
|
-
expect(content).toContain("declare module '@teardown/navigation'");
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("should generate manifest.json", () => {
|
|
268
|
-
createTestFile("index.tsx");
|
|
269
|
-
createTestFile("users/[userId].tsx");
|
|
270
|
-
|
|
271
|
-
generateAllRouteFiles({
|
|
272
|
-
routesDir: TEST_ROUTES_DIR,
|
|
273
|
-
generatedDir: TEST_GENERATED_DIR,
|
|
274
|
-
prefixes: [],
|
|
275
|
-
verbose: false,
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
const generatedPath = join(TEST_GENERATED_DIR, "manifest.json");
|
|
279
|
-
expect(existsSync(generatedPath)).toBe(true);
|
|
280
|
-
|
|
281
|
-
const content = JSON.parse(readFileSync(generatedPath, "utf-8"));
|
|
282
|
-
expect(content.routeCount).toBe(2);
|
|
283
|
-
expect(content.routes).toBeInstanceOf(Array);
|
|
284
|
-
expect(content.generatedAt).toBeDefined();
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
});
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Route generator for @teardown/navigation-metro
|
|
3
|
-
* Generates TypeScript type definitions from scanned route tree
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import { flattenRoutes, type ParamDefinition, type RouteNode, scanRoutesDirectory } from "../scanner/file-scanner";
|
|
9
|
-
|
|
10
|
-
export interface GenerateOptions {
|
|
11
|
-
routesDir: string;
|
|
12
|
-
generatedDir: string;
|
|
13
|
-
prefixes: string[];
|
|
14
|
-
verbose: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface RouteParamEntry {
|
|
18
|
-
path: string;
|
|
19
|
-
params: ParamDefinition[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Generates all route files (routes.generated.ts, linking.generated.ts, register.d.ts, manifest.json)
|
|
24
|
-
*/
|
|
25
|
-
export function generateAllRouteFiles(options: GenerateOptions): void {
|
|
26
|
-
const { routesDir, generatedDir, prefixes, verbose } = options;
|
|
27
|
-
|
|
28
|
-
// Ensure generated directory exists
|
|
29
|
-
mkdirSync(generatedDir, { recursive: true });
|
|
30
|
-
|
|
31
|
-
// Scan routes
|
|
32
|
-
const { routes, errors } = scanRoutesDirectory(routesDir);
|
|
33
|
-
|
|
34
|
-
if (errors.length > 0 && verbose) {
|
|
35
|
-
console.warn("[teardown/navigation] Scan warnings:", errors);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Generate files
|
|
39
|
-
const routesContent = generateRoutesFileContent(routes);
|
|
40
|
-
writeFileSync(join(generatedDir, "routes.generated.ts"), routesContent);
|
|
41
|
-
|
|
42
|
-
const linkingContent = generateLinkingFileContent(routes, prefixes);
|
|
43
|
-
writeFileSync(join(generatedDir, "linking.generated.ts"), linkingContent);
|
|
44
|
-
|
|
45
|
-
const registerContent = generateRegisterFileContent();
|
|
46
|
-
writeFileSync(join(generatedDir, "register.d.ts"), registerContent);
|
|
47
|
-
|
|
48
|
-
const manifestContent = generateManifestContent(routes);
|
|
49
|
-
writeFileSync(join(generatedDir, "manifest.json"), JSON.stringify(manifestContent, null, 2));
|
|
50
|
-
|
|
51
|
-
if (verbose) {
|
|
52
|
-
const count = countRoutes(routes);
|
|
53
|
-
console.log(`[teardown/navigation] Generated ${count} routes`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Builds the RouteParams interface entries from the route tree
|
|
59
|
-
* Each route has all its params extracted from its full path, so no accumulation needed
|
|
60
|
-
*/
|
|
61
|
-
export function buildRouteParamsInterface(routes: RouteNode[]): RouteParamEntry[] {
|
|
62
|
-
const result: RouteParamEntry[] = [];
|
|
63
|
-
|
|
64
|
-
function traverse(nodes: RouteNode[]): void {
|
|
65
|
-
for (const node of nodes) {
|
|
66
|
-
if (node.isLayout) {
|
|
67
|
-
// Layouts don't get their own route entry
|
|
68
|
-
traverse(node.children);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
result.push({
|
|
73
|
-
path: node.path,
|
|
74
|
-
params: node.params,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if (node.children.length > 0) {
|
|
78
|
-
traverse(node.children);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
traverse(routes);
|
|
84
|
-
return result;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Generates the routes.generated.ts file content
|
|
89
|
-
*/
|
|
90
|
-
export function generateRoutesFileContent(routes: RouteNode[]): string {
|
|
91
|
-
const routeParams = buildRouteParamsInterface(routes);
|
|
92
|
-
|
|
93
|
-
const lines: string[] = [
|
|
94
|
-
"// Auto-generated by @teardown/navigation",
|
|
95
|
-
"// Do not edit this file directly",
|
|
96
|
-
`// Generated at: ${new Date().toISOString()}`,
|
|
97
|
-
"",
|
|
98
|
-
"export interface RouteParams {",
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
// Add route param entries
|
|
102
|
-
for (const { path, params } of routeParams) {
|
|
103
|
-
const paramsType =
|
|
104
|
-
params.length === 0
|
|
105
|
-
? "undefined"
|
|
106
|
-
: `{ ${params
|
|
107
|
-
.map((p) => {
|
|
108
|
-
const type = p.isCatchAll ? "string[]" : "string";
|
|
109
|
-
return `${p.name}${p.isOptional ? "?" : ""}: ${type}`;
|
|
110
|
-
})
|
|
111
|
-
.join("; ")} }`;
|
|
112
|
-
|
|
113
|
-
lines.push(`\t"${path}": ${paramsType};`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
lines.push("}");
|
|
117
|
-
lines.push("");
|
|
118
|
-
|
|
119
|
-
// Add type aliases
|
|
120
|
-
lines.push("export type RoutePath = keyof RouteParams;");
|
|
121
|
-
lines.push("");
|
|
122
|
-
lines.push("export type ParamsFor<T extends RoutePath> = RouteParams[T];");
|
|
123
|
-
lines.push("");
|
|
124
|
-
|
|
125
|
-
// Add RouteWithParams utility type
|
|
126
|
-
lines.push("export type RouteWithParams = {");
|
|
127
|
-
lines.push("\t[K in RoutePath]: RouteParams[K] extends undefined");
|
|
128
|
-
lines.push("\t\t? { path: K }");
|
|
129
|
-
lines.push("\t\t: { path: K; params: RouteParams[K] };");
|
|
130
|
-
lines.push("}[RoutePath];");
|
|
131
|
-
lines.push("");
|
|
132
|
-
|
|
133
|
-
// Add NavigatorType
|
|
134
|
-
lines.push('export type NavigatorType = "stack" | "tabs" | "drawer";');
|
|
135
|
-
lines.push("");
|
|
136
|
-
|
|
137
|
-
return lines.join("\n");
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Generates the linking.generated.ts file content
|
|
142
|
-
*/
|
|
143
|
-
export function generateLinkingFileContent(routes: RouteNode[], prefixes: string[]): string {
|
|
144
|
-
const routeParams = buildRouteParamsInterface(routes);
|
|
145
|
-
|
|
146
|
-
const lines: string[] = [
|
|
147
|
-
"// Auto-generated by @teardown/navigation",
|
|
148
|
-
'import type { LinkingOptions } from "@react-navigation/native";',
|
|
149
|
-
'import type { RouteParams } from "./routes.generated";',
|
|
150
|
-
"",
|
|
151
|
-
];
|
|
152
|
-
|
|
153
|
-
// Build screens config
|
|
154
|
-
const screens: Record<string, string> = {};
|
|
155
|
-
for (const { path } of routeParams) {
|
|
156
|
-
// Convert /users/:userId to users/:userId (remove leading slash)
|
|
157
|
-
const linkingPath = path === "/" ? "" : path.slice(1);
|
|
158
|
-
screens[path] = linkingPath;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
lines.push('export const generatedLinkingConfig: LinkingOptions<RouteParams>["config"] = {');
|
|
162
|
-
lines.push("\tscreens: {");
|
|
163
|
-
|
|
164
|
-
for (const [routePath, linkingPath] of Object.entries(screens)) {
|
|
165
|
-
lines.push(`\t\t"${routePath}": "${linkingPath}",`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
lines.push("\t},");
|
|
169
|
-
lines.push("};");
|
|
170
|
-
lines.push("");
|
|
171
|
-
|
|
172
|
-
// Add prefixes
|
|
173
|
-
lines.push(`export const defaultPrefixes: string[] = ${JSON.stringify(prefixes)};`);
|
|
174
|
-
lines.push("");
|
|
175
|
-
|
|
176
|
-
return lines.join("\n");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Generates the register.d.ts file content
|
|
181
|
-
*/
|
|
182
|
-
export function generateRegisterFileContent(): string {
|
|
183
|
-
const lines: string[] = [
|
|
184
|
-
"// Auto-generated by @teardown/navigation",
|
|
185
|
-
'import type { RouteParams, RoutePath } from "./routes.generated";',
|
|
186
|
-
"",
|
|
187
|
-
"declare module '@teardown/navigation' {",
|
|
188
|
-
"\tinterface Register {",
|
|
189
|
-
"\t\trouteParams: RouteParams;",
|
|
190
|
-
"\t\troutePath: RoutePath;",
|
|
191
|
-
"\t}",
|
|
192
|
-
"}",
|
|
193
|
-
"",
|
|
194
|
-
];
|
|
195
|
-
|
|
196
|
-
return lines.join("\n");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Generates the manifest.json content
|
|
201
|
-
*/
|
|
202
|
-
function generateManifestContent(routes: RouteNode[]): {
|
|
203
|
-
generatedAt: string;
|
|
204
|
-
routeCount: number;
|
|
205
|
-
routes: Array<{
|
|
206
|
-
path: string;
|
|
207
|
-
file: string;
|
|
208
|
-
params: ParamDefinition[];
|
|
209
|
-
layoutType: string;
|
|
210
|
-
}>;
|
|
211
|
-
} {
|
|
212
|
-
const allRoutes = flattenRoutes(routes);
|
|
213
|
-
|
|
214
|
-
return {
|
|
215
|
-
generatedAt: new Date().toISOString(),
|
|
216
|
-
routeCount: allRoutes.filter((r) => !r.isLayout).length,
|
|
217
|
-
routes: allRoutes.map((r) => ({
|
|
218
|
-
path: r.path,
|
|
219
|
-
file: r.relativePath,
|
|
220
|
-
params: r.params,
|
|
221
|
-
layoutType: r.layoutType,
|
|
222
|
-
})),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Counts the number of non-layout routes
|
|
228
|
-
*/
|
|
229
|
-
function countRoutes(routes: RouteNode[]): number {
|
|
230
|
-
return flattenRoutes(routes).filter((r) => !r.isLayout).length;
|
|
231
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @teardown/navigation-metro
|
|
3
|
-
* Metro plugin for type-safe file-based navigation
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { resolve } from "node:path";
|
|
7
|
-
import { generateAllRouteFiles } from "./generator/route-generator";
|
|
8
|
-
import { startRouteWatcher } from "./watcher/file-watcher";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Options for the Teardown Navigation Metro plugin
|
|
12
|
-
*/
|
|
13
|
-
export interface TeardownNavigationOptions {
|
|
14
|
-
/**
|
|
15
|
-
* Path to routes directory relative to project root
|
|
16
|
-
* @default './src/routes'
|
|
17
|
-
*/
|
|
18
|
-
routesDir?: string;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Path for generated type files
|
|
22
|
-
* @default './.teardown'
|
|
23
|
-
*/
|
|
24
|
-
generatedDir?: string;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Deep link URL prefixes
|
|
28
|
-
* @default []
|
|
29
|
-
*/
|
|
30
|
-
prefixes?: string[];
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Enable verbose logging
|
|
34
|
-
* @default false
|
|
35
|
-
*/
|
|
36
|
-
verbose?: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Metro configuration type (simplified)
|
|
41
|
-
* Full type from metro-config can be used if available
|
|
42
|
-
*/
|
|
43
|
-
export interface MetroConfig {
|
|
44
|
-
projectRoot?: string;
|
|
45
|
-
watchFolders?: string[];
|
|
46
|
-
resolver?: {
|
|
47
|
-
resolveRequest?: (
|
|
48
|
-
context: unknown,
|
|
49
|
-
moduleName: string,
|
|
50
|
-
platform: string | null
|
|
51
|
-
) => { filePath: string; type: string } | null;
|
|
52
|
-
[key: string]: unknown;
|
|
53
|
-
};
|
|
54
|
-
transformer?: {
|
|
55
|
-
unstable_allowRequireContext?: boolean;
|
|
56
|
-
[key: string]: unknown;
|
|
57
|
-
};
|
|
58
|
-
[key: string]: unknown;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Wraps a Metro configuration with Teardown Navigation support
|
|
63
|
-
*
|
|
64
|
-
* This function:
|
|
65
|
-
* 1. Generates TypeScript type definitions on startup
|
|
66
|
-
* 2. Watches for route file changes in development
|
|
67
|
-
* 3. Configures Metro to include generated files
|
|
68
|
-
*
|
|
69
|
-
* @example
|
|
70
|
-
* ```js
|
|
71
|
-
* // metro.config.js
|
|
72
|
-
* const { getDefaultConfig } = require('expo/metro-config');
|
|
73
|
-
* const { withTeardownNavigation } = require('@teardown/navigation-metro');
|
|
74
|
-
*
|
|
75
|
-
* const config = getDefaultConfig(__dirname);
|
|
76
|
-
*
|
|
77
|
-
* module.exports = withTeardownNavigation(config, {
|
|
78
|
-
* routesDir: './src/routes',
|
|
79
|
-
* generatedDir: './.teardown',
|
|
80
|
-
* prefixes: ['myapp://', 'https://myapp.com'],
|
|
81
|
-
* verbose: true,
|
|
82
|
-
* });
|
|
83
|
-
* ```
|
|
84
|
-
*/
|
|
85
|
-
export function withTeardownNavigation(config: MetroConfig, options: TeardownNavigationOptions = {}): MetroConfig {
|
|
86
|
-
const { routesDir = "./src/routes", generatedDir = "./.teardown", prefixes = [], verbose = false } = options;
|
|
87
|
-
|
|
88
|
-
const projectRoot = config.projectRoot ?? process.cwd();
|
|
89
|
-
const absoluteRoutesDir = resolve(projectRoot, routesDir);
|
|
90
|
-
const absoluteGeneratedDir = resolve(projectRoot, generatedDir);
|
|
91
|
-
|
|
92
|
-
// Generate types on startup
|
|
93
|
-
try {
|
|
94
|
-
generateAllRouteFiles({
|
|
95
|
-
routesDir: absoluteRoutesDir,
|
|
96
|
-
generatedDir: absoluteGeneratedDir,
|
|
97
|
-
prefixes,
|
|
98
|
-
verbose,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
if (verbose) {
|
|
102
|
-
console.log("[teardown/navigation] Initial generation complete");
|
|
103
|
-
}
|
|
104
|
-
} catch (error) {
|
|
105
|
-
console.error("[teardown/navigation] Initial generation failed:", error);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Start file watcher in development
|
|
109
|
-
if (process.env.NODE_ENV !== "production") {
|
|
110
|
-
startRouteWatcher({
|
|
111
|
-
routesDir: absoluteRoutesDir,
|
|
112
|
-
generatedDir: absoluteGeneratedDir,
|
|
113
|
-
prefixes,
|
|
114
|
-
verbose,
|
|
115
|
-
onRegenerate: () => {
|
|
116
|
-
if (verbose) {
|
|
117
|
-
console.log("[teardown/navigation] Routes regenerated");
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
onError: (_errors) => {
|
|
121
|
-
if (verbose) {
|
|
122
|
-
console.error("[teardown/navigation] Validation errors during watch");
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Add watch folders for Metro
|
|
129
|
-
const watchFolders = [...(config.watchFolders ?? []), absoluteRoutesDir, absoluteGeneratedDir];
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
...config,
|
|
133
|
-
watchFolders,
|
|
134
|
-
transformer: {
|
|
135
|
-
...config.transformer,
|
|
136
|
-
unstable_allowRequireContext: true,
|
|
137
|
-
},
|
|
138
|
-
resolver: {
|
|
139
|
-
...config.resolver,
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export type { GenerateOptions, RouteParamEntry } from "./generator/route-generator";
|
|
145
|
-
// Re-export modules
|
|
146
|
-
export { generateAllRouteFiles } from "./generator/route-generator";
|
|
147
|
-
export type { ParamDefinition, RouteNode, ScanError, ScanResult } from "./scanner/file-scanner";
|
|
148
|
-
export {
|
|
149
|
-
buildUrlPath,
|
|
150
|
-
extractParams,
|
|
151
|
-
filePathToScreenName,
|
|
152
|
-
flattenRoutes,
|
|
153
|
-
scanRoutesDirectory,
|
|
154
|
-
} from "./scanner/file-scanner";
|
|
155
|
-
export type { ValidationError } from "./validator/route-validator";
|
|
156
|
-
export { validateRoutes } from "./validator/route-validator";
|
|
157
|
-
export type { WatcherOptions } from "./watcher/file-watcher";
|
|
158
|
-
export { isWatcherRunning, startRouteWatcher, stopRouteWatcher } from "./watcher/file-watcher";
|