@teardown/navigation-metro 2.0.43

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,178 @@
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
+ }
@@ -0,0 +1,132 @@
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
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Watcher module for @teardown/navigation-metro
3
+ */
4
+
5
+ export { isWatcherRunning, startRouteWatcher, stopRouteWatcher, type WatcherOptions } from "./file-watcher";