@teardown/navigation-metro 2.0.53 → 2.0.57

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.
Files changed (38) hide show
  1. package/dist/generator/index.d.ts +5 -0
  2. package/dist/generator/index.d.ts.map +1 -0
  3. package/dist/generator/index.js +12 -0
  4. package/dist/generator/route-generator.d.ts +37 -0
  5. package/dist/generator/route-generator.d.ts.map +1 -0
  6. package/dist/generator/route-generator.js +179 -0
  7. package/dist/index.d.ts +83 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +103 -0
  10. package/dist/scanner/file-scanner.d.ts +62 -0
  11. package/dist/scanner/file-scanner.d.ts.map +1 -0
  12. package/dist/scanner/file-scanner.js +250 -0
  13. package/dist/scanner/index.d.ts +5 -0
  14. package/dist/scanner/index.d.ts.map +1 -0
  15. package/dist/scanner/index.js +12 -0
  16. package/{src/validator/index.ts → dist/validator/index.d.ts} +1 -1
  17. package/dist/validator/index.d.ts.map +1 -0
  18. package/dist/validator/index.js +8 -0
  19. package/dist/validator/route-validator.d.ts +15 -0
  20. package/dist/validator/route-validator.d.ts.map +1 -0
  21. package/dist/validator/route-validator.js +153 -0
  22. package/dist/watcher/file-watcher.d.ts +27 -0
  23. package/dist/watcher/file-watcher.d.ts.map +1 -0
  24. package/dist/watcher/file-watcher.js +110 -0
  25. package/{src/watcher/index.ts → dist/watcher/index.d.ts} +1 -1
  26. package/dist/watcher/index.d.ts.map +1 -0
  27. package/dist/watcher/index.js +10 -0
  28. package/package.json +12 -9
  29. package/src/generator/index.ts +0 -13
  30. package/src/generator/route-generator.test.ts +0 -287
  31. package/src/generator/route-generator.ts +0 -231
  32. package/src/index.ts +0 -158
  33. package/src/scanner/file-scanner.test.ts +0 -271
  34. package/src/scanner/file-scanner.ts +0 -329
  35. package/src/scanner/index.ts +0 -15
  36. package/src/validator/route-validator.test.ts +0 -192
  37. package/src/validator/route-validator.ts +0 -178
  38. package/src/watcher/file-watcher.ts +0 -132
@@ -1,192 +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 { validateRoutes } from "./route-validator";
5
-
6
- const TEST_DIR = join(import.meta.dir, "__test_routes__");
7
-
8
- function createTestFile(relativePath: string, content: string) {
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("Route Validator", () => {
16
- beforeEach(() => {
17
- mkdirSync(TEST_DIR, { recursive: true });
18
- });
19
-
20
- afterEach(() => {
21
- rmSync(TEST_DIR, { recursive: true, force: true });
22
- });
23
-
24
- describe("validateRoutes", () => {
25
- it("should return error for non-existent directory", () => {
26
- const errors = validateRoutes("/non/existent/path");
27
- expect(errors.length).toBeGreaterThan(0);
28
- expect(errors[0].severity).toBe("error");
29
- });
30
-
31
- it("should return no errors for valid simple route", () => {
32
- createTestFile(
33
- "about.tsx",
34
- `
35
- export default function About() {
36
- return null;
37
- }
38
- `
39
- );
40
-
41
- const errors = validateRoutes(TEST_DIR);
42
- const actualErrors = errors.filter((e) => e.severity === "error");
43
- expect(actualErrors).toEqual([]);
44
- });
45
-
46
- it("should detect missing default export", () => {
47
- createTestFile(
48
- "about.tsx",
49
- `
50
- export function About() {
51
- return null;
52
- }
53
- `
54
- );
55
-
56
- const errors = validateRoutes(TEST_DIR);
57
- const missingExport = errors.find((e) => e.message.includes("default export"));
58
- expect(missingExport).toBeDefined();
59
- expect(missingExport?.severity).toBe("error");
60
- });
61
-
62
- it("should detect duplicate route paths", () => {
63
- // Create two routes that resolve to the same path
64
- createTestFile("users/index.tsx", "export default function UsersIndex() {}");
65
- // We can't easily create duplicate paths with file-based routing
66
- // but we can simulate by checking the validator handles it
67
- // For now, just check that the validator runs without error
68
- const errors = validateRoutes(TEST_DIR);
69
- expect(errors).toBeDefined();
70
- });
71
-
72
- it("should warn when screen doesn't use defineScreen", () => {
73
- createTestFile(
74
- "about.tsx",
75
- `
76
- export default function About() {
77
- return null;
78
- }
79
- `
80
- );
81
-
82
- const errors = validateRoutes(TEST_DIR);
83
- const warning = errors.find((e) => e.message.includes("defineScreen") && e.severity === "warning");
84
- expect(warning).toBeDefined();
85
- });
86
-
87
- it("should not warn when screen uses defineScreen", () => {
88
- createTestFile(
89
- "about.tsx",
90
- `
91
- import { defineScreen } from '@teardown/navigation';
92
-
93
- function AboutScreen() {
94
- return null;
95
- }
96
-
97
- export default defineScreen({
98
- component: AboutScreen,
99
- });
100
- `
101
- );
102
-
103
- const errors = validateRoutes(TEST_DIR);
104
- const defineScreenWarning = errors.find((e) => e.message.includes("defineScreen") && e.file.includes("about"));
105
- expect(defineScreenWarning).toBeUndefined();
106
- });
107
-
108
- it("should warn when layout doesn't use defineLayout", () => {
109
- createTestFile(
110
- "_layout.tsx",
111
- `
112
- export default {
113
- type: 'stack'
114
- };
115
- `
116
- );
117
-
118
- const errors = validateRoutes(TEST_DIR);
119
- const warning = errors.find((e) => e.message.includes("defineLayout"));
120
- expect(warning).toBeDefined();
121
- expect(warning?.severity).toBe("warning");
122
- });
123
-
124
- it("should not warn when layout uses defineLayout", () => {
125
- createTestFile(
126
- "_layout.tsx",
127
- `
128
- import { defineLayout } from '@teardown/navigation';
129
-
130
- export default defineLayout({
131
- type: 'stack'
132
- });
133
- `
134
- );
135
-
136
- const errors = validateRoutes(TEST_DIR);
137
- const defineLayoutWarning = errors.find((e) => e.message.includes("defineLayout") && e.file.includes("_layout"));
138
- expect(defineLayoutWarning).toBeUndefined();
139
- });
140
-
141
- it("should warn when dynamic route lacks param schema", () => {
142
- createTestFile(
143
- "users/[userId].tsx",
144
- `
145
- export default function UserProfile() {
146
- return null;
147
- }
148
- `
149
- );
150
-
151
- const errors = validateRoutes(TEST_DIR);
152
- const warning = errors.find((e) => e.message.includes("param") && e.message.includes("schema"));
153
- expect(warning).toBeDefined();
154
- expect(warning?.severity).toBe("warning");
155
- });
156
-
157
- it("should not warn when dynamic route has param schema", () => {
158
- createTestFile(
159
- "users/[userId].tsx",
160
- `
161
- import { createParamSchema, paramValidators } from '@teardown/navigation';
162
-
163
- export const paramsSchema = createParamSchema({
164
- userId: paramValidators.uuid(),
165
- });
166
-
167
- export default function UserProfile() {
168
- return null;
169
- }
170
- `
171
- );
172
-
173
- const errors = validateRoutes(TEST_DIR);
174
- const schemaWarning = errors.find(
175
- (e) => e.message.includes("param") && e.message.includes("schema") && e.file.includes("[userId]")
176
- );
177
- expect(schemaWarning).toBeUndefined();
178
- });
179
-
180
- it("should validate nested routes", () => {
181
- createTestFile("_layout.tsx", "export default { type: 'stack' }");
182
- createTestFile("users/_layout.tsx", "export default { type: 'stack' }");
183
- createTestFile("users/index.tsx", "export default function Users() {}");
184
- createTestFile("users/[userId].tsx", "export default function User() {}");
185
-
186
- const errors = validateRoutes(TEST_DIR);
187
- // Should have some warnings but no errors
188
- const actualErrors = errors.filter((e) => e.severity === "error");
189
- expect(actualErrors).toEqual([]);
190
- });
191
- });
192
- });
@@ -1,178 +0,0 @@
1
- /**
2
- * Route validator for @teardown/navigation-metro
3
- * Validates route files for common issues and best practices
4
- */
5
-
6
- import { readFileSync } from "node:fs";
7
- import { flattenRoutes, type RouteNode, scanRoutesDirectory } from "../scanner/file-scanner";
8
-
9
- export interface ValidationError {
10
- file: string;
11
- message: string;
12
- severity: "error" | "warning";
13
- line?: number;
14
- }
15
-
16
- /**
17
- * Validates all routes in a directory
18
- */
19
- export function validateRoutes(routesDir: string): ValidationError[] {
20
- const errors: ValidationError[] = [];
21
- const { routes, errors: scanErrors } = scanRoutesDirectory(routesDir);
22
-
23
- // Add scan errors
24
- errors.push(
25
- ...scanErrors.map((e) => ({
26
- file: e.file,
27
- message: e.message,
28
- severity: "error" as const,
29
- }))
30
- );
31
-
32
- // If the directory doesn't exist, we already have an error
33
- if (scanErrors.length > 0 && routes.length === 0) {
34
- return errors;
35
- }
36
-
37
- // Flatten routes for validation
38
- const flatRoutes = flattenRoutes(routes);
39
- const seenPaths = new Map<string, string>();
40
-
41
- for (const route of flatRoutes) {
42
- // Skip layout files for path uniqueness check
43
- if (route.isLayout) {
44
- validateLayoutFile(route, errors);
45
- continue;
46
- }
47
-
48
- // Check for duplicate paths
49
- if (seenPaths.has(route.path)) {
50
- errors.push({
51
- file: route.filePath,
52
- message: `Duplicate route path "${route.path}" (also defined in ${seenPaths.get(route.path)})`,
53
- severity: "error",
54
- });
55
- } else {
56
- seenPaths.set(route.path, route.filePath);
57
- }
58
-
59
- // Validate screen file
60
- validateScreenFile(route, errors);
61
- }
62
-
63
- return errors;
64
- }
65
-
66
- /**
67
- * Validates a screen file
68
- */
69
- function validateScreenFile(route: RouteNode, errors: ValidationError[]): void {
70
- const content = safeReadFile(route.filePath);
71
- if (content === null) {
72
- errors.push({
73
- file: route.filePath,
74
- message: "Could not read route file",
75
- severity: "error",
76
- });
77
- return;
78
- }
79
-
80
- // Check for default export
81
- if (!hasDefaultExport(content)) {
82
- errors.push({
83
- file: route.filePath,
84
- message: "Route file must have a default export",
85
- severity: "error",
86
- });
87
- }
88
-
89
- // Check for defineScreen usage
90
- if (!usesDefineScreen(content)) {
91
- errors.push({
92
- file: route.filePath,
93
- message: "Screen should use defineScreen() for proper typing",
94
- severity: "warning",
95
- });
96
- }
97
-
98
- // Check dynamic routes have param schema
99
- if (route.params.length > 0 && !hasParamSchema(content)) {
100
- errors.push({
101
- file: route.filePath,
102
- message: "Dynamic route should export a params schema for runtime validation",
103
- severity: "warning",
104
- });
105
- }
106
- }
107
-
108
- /**
109
- * Validates a layout file
110
- */
111
- function validateLayoutFile(route: RouteNode, errors: ValidationError[]): void {
112
- const content = safeReadFile(route.filePath);
113
- if (content === null) {
114
- errors.push({
115
- file: route.filePath,
116
- message: "Could not read layout file",
117
- severity: "error",
118
- });
119
- return;
120
- }
121
-
122
- // Check for default export
123
- if (!hasDefaultExport(content)) {
124
- errors.push({
125
- file: route.filePath,
126
- message: "Layout file must have a default export",
127
- severity: "error",
128
- });
129
- }
130
-
131
- // Check for defineLayout usage
132
- if (!usesDefineLayout(content)) {
133
- errors.push({
134
- file: route.filePath,
135
- message: "Layout file should use defineLayout() for proper configuration",
136
- severity: "warning",
137
- });
138
- }
139
- }
140
-
141
- /**
142
- * Safely reads a file, returning null on error
143
- */
144
- function safeReadFile(filePath: string): string | null {
145
- try {
146
- return readFileSync(filePath, "utf-8");
147
- } catch {
148
- return null;
149
- }
150
- }
151
-
152
- /**
153
- * Checks if file has a default export
154
- */
155
- function hasDefaultExport(content: string): boolean {
156
- return content.includes("export default");
157
- }
158
-
159
- /**
160
- * Checks if file uses defineScreen
161
- */
162
- function usesDefineScreen(content: string): boolean {
163
- return content.includes("defineScreen");
164
- }
165
-
166
- /**
167
- * Checks if file uses defineLayout
168
- */
169
- function usesDefineLayout(content: string): boolean {
170
- return content.includes("defineLayout");
171
- }
172
-
173
- /**
174
- * Checks if file has a param schema
175
- */
176
- function hasParamSchema(content: string): boolean {
177
- return content.includes("paramsSchema") || content.includes("createParamSchema");
178
- }
@@ -1,132 +0,0 @@
1
- /**
2
- * File watcher for @teardown/navigation-metro
3
- * Watches route files and triggers regeneration on changes
4
- */
5
-
6
- import { join, relative } from "node:path";
7
- import { type FSWatcher, watch } from "chokidar";
8
- import { generateAllRouteFiles } from "../generator/route-generator";
9
- import { type ValidationError, validateRoutes } from "../validator/route-validator";
10
-
11
- export interface WatcherOptions {
12
- routesDir: string;
13
- generatedDir: string;
14
- prefixes: string[];
15
- verbose: boolean;
16
- onRegenerate?: () => void;
17
- onError?: (errors: ValidationError[]) => void;
18
- }
19
-
20
- let watcherInstance: FSWatcher | null = null;
21
-
22
- /**
23
- * Starts watching the routes directory for changes
24
- * Returns a cleanup function to stop watching
25
- */
26
- export function startRouteWatcher(options: WatcherOptions): () => void {
27
- const { routesDir, generatedDir, prefixes, verbose, onRegenerate, onError } = options;
28
-
29
- // Close existing watcher if any
30
- if (watcherInstance) {
31
- watcherInstance.close();
32
- }
33
-
34
- const watcher = watch(join(routesDir, "**/*.{ts,tsx}"), {
35
- ignoreInitial: true,
36
- ignored: [
37
- /(^|[/\\])\../, // Dotfiles
38
- /node_modules/,
39
- /\.test\./,
40
- /\.spec\./,
41
- ],
42
- });
43
-
44
- let debounceTimer: ReturnType<typeof setTimeout> | null = null;
45
-
46
- const regenerate = () => {
47
- if (debounceTimer) {
48
- clearTimeout(debounceTimer);
49
- }
50
-
51
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: debounced validate + generate
52
- debounceTimer = setTimeout(() => {
53
- try {
54
- // Validate first
55
- const errors = validateRoutes(routesDir);
56
- const hasErrors = errors.some((e) => e.severity === "error");
57
-
58
- if (hasErrors) {
59
- if (verbose) {
60
- console.error("[teardown/navigation] Validation errors:");
61
- for (const e of errors.filter((err) => err.severity === "error")) {
62
- console.error(` ${e.file}: ${e.message}`);
63
- }
64
- }
65
- onError?.(errors);
66
- return;
67
- }
68
-
69
- // Generate if validation passes
70
- generateAllRouteFiles({ routesDir, generatedDir, prefixes, verbose });
71
-
72
- if (verbose) {
73
- console.log("[teardown/navigation] Routes regenerated");
74
- }
75
-
76
- onRegenerate?.();
77
- } catch (error) {
78
- if (verbose) {
79
- console.error("[teardown/navigation] Generation failed:", error);
80
- }
81
- }
82
- }, 100);
83
- };
84
-
85
- watcher.on("add", (filePath) => {
86
- if (verbose) {
87
- console.log(`[teardown/navigation] File added: ${relative(routesDir, filePath)}`);
88
- }
89
- regenerate();
90
- });
91
-
92
- watcher.on("unlink", (filePath) => {
93
- if (verbose) {
94
- console.log(`[teardown/navigation] File removed: ${relative(routesDir, filePath)}`);
95
- }
96
- regenerate();
97
- });
98
-
99
- watcher.on("change", (filePath) => {
100
- if (verbose) {
101
- console.log(`[teardown/navigation] File changed: ${relative(routesDir, filePath)}`);
102
- }
103
- regenerate();
104
- });
105
-
106
- watcherInstance = watcher;
107
-
108
- return () => {
109
- if (debounceTimer) {
110
- clearTimeout(debounceTimer);
111
- }
112
- watcher.close();
113
- watcherInstance = null;
114
- };
115
- }
116
-
117
- /**
118
- * Stops the route watcher if it's running
119
- */
120
- export function stopRouteWatcher(): void {
121
- if (watcherInstance) {
122
- watcherInstance.close();
123
- watcherInstance = null;
124
- }
125
- }
126
-
127
- /**
128
- * Checks if the watcher is currently running
129
- */
130
- export function isWatcherRunning(): boolean {
131
- return watcherInstance !== null;
132
- }