@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
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* File scanner for @teardown/navigation-metro
|
|
4
|
+
* Scans routes directory and builds a route tree for type generation
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.scanRoutesDirectory = scanRoutesDirectory;
|
|
8
|
+
exports.extractParams = extractParams;
|
|
9
|
+
exports.buildUrlPath = buildUrlPath;
|
|
10
|
+
exports.filePathToScreenName = filePathToScreenName;
|
|
11
|
+
exports.flattenRoutes = flattenRoutes;
|
|
12
|
+
const node_fs_1 = require("node:fs");
|
|
13
|
+
const node_path_1 = require("node:path");
|
|
14
|
+
/**
|
|
15
|
+
* Scans a routes directory and builds a route tree
|
|
16
|
+
*/
|
|
17
|
+
function scanRoutesDirectory(routesDir) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
if (!(0, node_fs_1.existsSync)(routesDir)) {
|
|
20
|
+
return {
|
|
21
|
+
routes: [],
|
|
22
|
+
errors: [{ file: routesDir, message: "Routes directory does not exist" }],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const files = findRouteFiles(routesDir);
|
|
26
|
+
const routeNodes = new Map();
|
|
27
|
+
// First pass: create all route nodes
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const absolutePath = (0, node_path_1.join)(routesDir, file);
|
|
30
|
+
const node = parseRouteFile(file, absolutePath);
|
|
31
|
+
if (node) {
|
|
32
|
+
routeNodes.set(file, node);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Second pass: build tree structure
|
|
36
|
+
const rootNodes = [];
|
|
37
|
+
for (const [filePath, node] of routeNodes) {
|
|
38
|
+
const parentPath = findParentLayoutPath(filePath, routeNodes);
|
|
39
|
+
if (parentPath) {
|
|
40
|
+
const parent = routeNodes.get(parentPath);
|
|
41
|
+
if (parent) {
|
|
42
|
+
parent.children.push(node);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
rootNodes.push(node);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { routes: rootNodes, errors };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Recursively finds all route files in a directory
|
|
53
|
+
*/
|
|
54
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: route file discovery branches
|
|
55
|
+
function findRouteFiles(dir, prefix = "") {
|
|
56
|
+
const results = [];
|
|
57
|
+
const entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
// Skip node_modules and hidden directories (except those in parentheses for groups)
|
|
62
|
+
if (entry.name === "node_modules" || (entry.name.startsWith(".") && !entry.name.startsWith("("))) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
results.push(...findRouteFiles((0, node_path_1.join)(dir, entry.name), relativePath));
|
|
66
|
+
}
|
|
67
|
+
else if (entry.isFile()) {
|
|
68
|
+
// Only include .ts and .tsx files
|
|
69
|
+
const ext = (0, node_path_1.extname)(entry.name);
|
|
70
|
+
if (ext !== ".ts" && ext !== ".tsx") {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// Skip test files
|
|
74
|
+
if (entry.name.includes(".test.") || entry.name.includes(".spec.")) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// Skip hidden files (starting with _) except _layout
|
|
78
|
+
const baseName = (0, node_path_1.basename)(entry.name, ext);
|
|
79
|
+
if (baseName.startsWith("_") && baseName !== "_layout") {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
results.push(relativePath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Parses a route file and creates a RouteNode
|
|
89
|
+
*/
|
|
90
|
+
function parseRouteFile(relativePath, absolutePath) {
|
|
91
|
+
const ext = (0, node_path_1.extname)(relativePath);
|
|
92
|
+
const fileName = (0, node_path_1.basename)(relativePath, ext);
|
|
93
|
+
const isLayout = fileName === "_layout";
|
|
94
|
+
const isIndex = fileName === "index";
|
|
95
|
+
const isCatchAll = fileName.startsWith("[...");
|
|
96
|
+
// Extract route group from path
|
|
97
|
+
const groupMatch = relativePath.match(/\(([^)]+)\)/);
|
|
98
|
+
const groupName = groupMatch ? groupMatch[1] : null;
|
|
99
|
+
// Parse params from the entire relative path (not just filename)
|
|
100
|
+
// This extracts params from both directory names and the filename
|
|
101
|
+
const params = extractParams(relativePath);
|
|
102
|
+
// Build URL path
|
|
103
|
+
const urlPath = buildUrlPath(relativePath, isIndex, isLayout);
|
|
104
|
+
return {
|
|
105
|
+
name: filePathToScreenName(relativePath),
|
|
106
|
+
path: urlPath,
|
|
107
|
+
filePath: absolutePath,
|
|
108
|
+
relativePath,
|
|
109
|
+
params,
|
|
110
|
+
children: [],
|
|
111
|
+
layoutType: isLayout ? detectLayoutType(absolutePath) : "none",
|
|
112
|
+
isIndex,
|
|
113
|
+
isLayout,
|
|
114
|
+
isCatchAll,
|
|
115
|
+
groupName,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Extracts dynamic parameters from a filename
|
|
120
|
+
*/
|
|
121
|
+
function extractParams(fileName) {
|
|
122
|
+
const params = [];
|
|
123
|
+
const regex = /\[(?:\.\.\.)?([^\]]+)\]\??/g;
|
|
124
|
+
let match;
|
|
125
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex iteration pattern
|
|
126
|
+
while ((match = regex.exec(fileName)) !== null) {
|
|
127
|
+
const fullMatch = match[0];
|
|
128
|
+
const paramName = match[1].replace("?", "");
|
|
129
|
+
params.push({
|
|
130
|
+
name: paramName,
|
|
131
|
+
isOptional: fullMatch.endsWith("?"),
|
|
132
|
+
isCatchAll: fullMatch.startsWith("[..."),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return params;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Builds a URL path from a relative file path
|
|
139
|
+
*/
|
|
140
|
+
function buildUrlPath(relativePath, _isIndex, isLayout) {
|
|
141
|
+
if (isLayout)
|
|
142
|
+
return "";
|
|
143
|
+
const ext = (0, node_path_1.extname)(relativePath);
|
|
144
|
+
let urlPath = relativePath
|
|
145
|
+
.replace(ext, "") // Remove extension
|
|
146
|
+
.replace(/\\/g, "/") // Normalize path separators
|
|
147
|
+
.replace(/\(([^)]+)\)\//g, "") // Remove route groups from URL
|
|
148
|
+
.replace(/\[\.\.\.([^\]]+)\]/g, "*") // Catch-all
|
|
149
|
+
.replace(/\[([^\]]+)\]\?/g, ":$1?") // Optional params
|
|
150
|
+
.replace(/\[([^\]]+)\]/g, ":$1"); // Required params
|
|
151
|
+
// Handle index files - remove "index" or trailing "/index"
|
|
152
|
+
if (urlPath === "index") {
|
|
153
|
+
return "/";
|
|
154
|
+
}
|
|
155
|
+
urlPath = urlPath.replace(/\/index$/, "");
|
|
156
|
+
return `/${urlPath}` || "/";
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Converts a file path to a screen name
|
|
160
|
+
*/
|
|
161
|
+
function filePathToScreenName(relativePath) {
|
|
162
|
+
const ext = (0, node_path_1.extname)(relativePath);
|
|
163
|
+
return relativePath.replace(ext, "").replace(/\\/g, "/");
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Detects the layout type from a _layout.tsx file
|
|
167
|
+
*/
|
|
168
|
+
function detectLayoutType(absolutePath) {
|
|
169
|
+
try {
|
|
170
|
+
const content = (0, node_fs_1.readFileSync)(absolutePath, "utf-8");
|
|
171
|
+
if (content.includes("type: 'tabs'") || content.includes('type: "tabs"')) {
|
|
172
|
+
return "tabs";
|
|
173
|
+
}
|
|
174
|
+
if (content.includes("type: 'drawer'") || content.includes('type: "drawer"')) {
|
|
175
|
+
return "drawer";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Default to stack on read error
|
|
180
|
+
}
|
|
181
|
+
return "stack";
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Finds the parent layout file path for a given route file
|
|
185
|
+
*/
|
|
186
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: layout path traversal
|
|
187
|
+
function findParentLayoutPath(filePath, routeNodes) {
|
|
188
|
+
const dir = (0, node_path_1.dirname)(filePath);
|
|
189
|
+
const fileName = (0, node_path_1.basename)(filePath);
|
|
190
|
+
const isLayout = fileName === "_layout.tsx" || fileName === "_layout.ts";
|
|
191
|
+
// For root level files
|
|
192
|
+
if (dir === ".") {
|
|
193
|
+
// Check if there's a root _layout.tsx and this file is not the layout itself
|
|
194
|
+
const rootLayout = "_layout.tsx";
|
|
195
|
+
if (filePath !== rootLayout && routeNodes.has(rootLayout)) {
|
|
196
|
+
return rootLayout;
|
|
197
|
+
}
|
|
198
|
+
const rootLayoutTs = "_layout.ts";
|
|
199
|
+
if (filePath !== rootLayoutTs && routeNodes.has(rootLayoutTs)) {
|
|
200
|
+
return rootLayoutTs;
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
// For non-layout files, look for _layout in the same directory first
|
|
205
|
+
if (!isLayout) {
|
|
206
|
+
const sameDirLayout = `${dir}/_layout.tsx`;
|
|
207
|
+
const sameDirLayoutTs = `${dir}/_layout.ts`;
|
|
208
|
+
if (routeNodes.has(sameDirLayout)) {
|
|
209
|
+
return sameDirLayout;
|
|
210
|
+
}
|
|
211
|
+
if (routeNodes.has(sameDirLayoutTs)) {
|
|
212
|
+
return sameDirLayoutTs;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// For layouts, or if no layout in same directory, look in parent directories
|
|
216
|
+
let parentDir = (0, node_path_1.dirname)(dir);
|
|
217
|
+
while (parentDir !== ".") {
|
|
218
|
+
const layoutPath = `${parentDir}/_layout.tsx`;
|
|
219
|
+
const layoutPathTs = `${parentDir}/_layout.ts`;
|
|
220
|
+
if (routeNodes.has(layoutPath)) {
|
|
221
|
+
return layoutPath;
|
|
222
|
+
}
|
|
223
|
+
if (routeNodes.has(layoutPathTs)) {
|
|
224
|
+
return layoutPathTs;
|
|
225
|
+
}
|
|
226
|
+
parentDir = (0, node_path_1.dirname)(parentDir);
|
|
227
|
+
}
|
|
228
|
+
// Check root level layout
|
|
229
|
+
if (routeNodes.has("_layout.tsx") && filePath !== "_layout.tsx") {
|
|
230
|
+
return "_layout.tsx";
|
|
231
|
+
}
|
|
232
|
+
if (routeNodes.has("_layout.ts") && filePath !== "_layout.ts") {
|
|
233
|
+
return "_layout.ts";
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Flattens a route tree into a flat array
|
|
239
|
+
*/
|
|
240
|
+
function flattenRoutes(routes) {
|
|
241
|
+
const result = [];
|
|
242
|
+
function traverse(nodes) {
|
|
243
|
+
for (const node of nodes) {
|
|
244
|
+
result.push(node);
|
|
245
|
+
traverse(node.children);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
traverse(routes);
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scanner module for @teardown/navigation-metro
|
|
3
|
+
*/
|
|
4
|
+
export { buildUrlPath, extractParams, filePathToScreenName, flattenRoutes, type ParamDefinition, type RouteNode, type ScanError, type ScanResult, scanRoutesDirectory, } from "./file-scanner";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scanner/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,UAAU,EACf,mBAAmB,GACnB,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Scanner module for @teardown/navigation-metro
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.scanRoutesDirectory = exports.flattenRoutes = exports.filePathToScreenName = exports.extractParams = exports.buildUrlPath = void 0;
|
|
7
|
+
var file_scanner_1 = require("./file-scanner");
|
|
8
|
+
Object.defineProperty(exports, "buildUrlPath", { enumerable: true, get: function () { return file_scanner_1.buildUrlPath; } });
|
|
9
|
+
Object.defineProperty(exports, "extractParams", { enumerable: true, get: function () { return file_scanner_1.extractParams; } });
|
|
10
|
+
Object.defineProperty(exports, "filePathToScreenName", { enumerable: true, get: function () { return file_scanner_1.filePathToScreenName; } });
|
|
11
|
+
Object.defineProperty(exports, "flattenRoutes", { enumerable: true, get: function () { return file_scanner_1.flattenRoutes; } });
|
|
12
|
+
Object.defineProperty(exports, "scanRoutesDirectory", { enumerable: true, get: function () { return file_scanner_1.scanRoutesDirectory; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/validator/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,eAAe,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Validator module for @teardown/navigation-metro
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.validateRoutes = void 0;
|
|
7
|
+
var route_validator_1 = require("./route-validator");
|
|
8
|
+
Object.defineProperty(exports, "validateRoutes", { enumerable: true, get: function () { return route_validator_1.validateRoutes; } });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route validator for @teardown/navigation-metro
|
|
3
|
+
* Validates route files for common issues and best practices
|
|
4
|
+
*/
|
|
5
|
+
export interface ValidationError {
|
|
6
|
+
file: string;
|
|
7
|
+
message: string;
|
|
8
|
+
severity: "error" | "warning";
|
|
9
|
+
line?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Validates all routes in a directory
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateRoutes(routesDir: string): ValidationError[];
|
|
15
|
+
//# sourceMappingURL=route-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-validator.d.ts","sourceRoot":"","sources":["../../src/validator/route-validator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CA6CnE"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Route validator for @teardown/navigation-metro
|
|
4
|
+
* Validates route files for common issues and best practices
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.validateRoutes = validateRoutes;
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const file_scanner_1 = require("../scanner/file-scanner");
|
|
10
|
+
/**
|
|
11
|
+
* Validates all routes in a directory
|
|
12
|
+
*/
|
|
13
|
+
function validateRoutes(routesDir) {
|
|
14
|
+
const errors = [];
|
|
15
|
+
const { routes, errors: scanErrors } = (0, file_scanner_1.scanRoutesDirectory)(routesDir);
|
|
16
|
+
// Add scan errors
|
|
17
|
+
errors.push(...scanErrors.map((e) => ({
|
|
18
|
+
file: e.file,
|
|
19
|
+
message: e.message,
|
|
20
|
+
severity: "error",
|
|
21
|
+
})));
|
|
22
|
+
// If the directory doesn't exist, we already have an error
|
|
23
|
+
if (scanErrors.length > 0 && routes.length === 0) {
|
|
24
|
+
return errors;
|
|
25
|
+
}
|
|
26
|
+
// Flatten routes for validation
|
|
27
|
+
const flatRoutes = (0, file_scanner_1.flattenRoutes)(routes);
|
|
28
|
+
const seenPaths = new Map();
|
|
29
|
+
for (const route of flatRoutes) {
|
|
30
|
+
// Skip layout files for path uniqueness check
|
|
31
|
+
if (route.isLayout) {
|
|
32
|
+
validateLayoutFile(route, errors);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Check for duplicate paths
|
|
36
|
+
if (seenPaths.has(route.path)) {
|
|
37
|
+
errors.push({
|
|
38
|
+
file: route.filePath,
|
|
39
|
+
message: `Duplicate route path "${route.path}" (also defined in ${seenPaths.get(route.path)})`,
|
|
40
|
+
severity: "error",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
seenPaths.set(route.path, route.filePath);
|
|
45
|
+
}
|
|
46
|
+
// Validate screen file
|
|
47
|
+
validateScreenFile(route, errors);
|
|
48
|
+
}
|
|
49
|
+
return errors;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validates a screen file
|
|
53
|
+
*/
|
|
54
|
+
function validateScreenFile(route, errors) {
|
|
55
|
+
const content = safeReadFile(route.filePath);
|
|
56
|
+
if (content === null) {
|
|
57
|
+
errors.push({
|
|
58
|
+
file: route.filePath,
|
|
59
|
+
message: "Could not read route file",
|
|
60
|
+
severity: "error",
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Check for default export
|
|
65
|
+
if (!hasDefaultExport(content)) {
|
|
66
|
+
errors.push({
|
|
67
|
+
file: route.filePath,
|
|
68
|
+
message: "Route file must have a default export",
|
|
69
|
+
severity: "error",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Check for defineScreen usage
|
|
73
|
+
if (!usesDefineScreen(content)) {
|
|
74
|
+
errors.push({
|
|
75
|
+
file: route.filePath,
|
|
76
|
+
message: "Screen should use defineScreen() for proper typing",
|
|
77
|
+
severity: "warning",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Check dynamic routes have param schema
|
|
81
|
+
if (route.params.length > 0 && !hasParamSchema(content)) {
|
|
82
|
+
errors.push({
|
|
83
|
+
file: route.filePath,
|
|
84
|
+
message: "Dynamic route should export a params schema for runtime validation",
|
|
85
|
+
severity: "warning",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Validates a layout file
|
|
91
|
+
*/
|
|
92
|
+
function validateLayoutFile(route, errors) {
|
|
93
|
+
const content = safeReadFile(route.filePath);
|
|
94
|
+
if (content === null) {
|
|
95
|
+
errors.push({
|
|
96
|
+
file: route.filePath,
|
|
97
|
+
message: "Could not read layout file",
|
|
98
|
+
severity: "error",
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Check for default export
|
|
103
|
+
if (!hasDefaultExport(content)) {
|
|
104
|
+
errors.push({
|
|
105
|
+
file: route.filePath,
|
|
106
|
+
message: "Layout file must have a default export",
|
|
107
|
+
severity: "error",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// Check for defineLayout usage
|
|
111
|
+
if (!usesDefineLayout(content)) {
|
|
112
|
+
errors.push({
|
|
113
|
+
file: route.filePath,
|
|
114
|
+
message: "Layout file should use defineLayout() for proper configuration",
|
|
115
|
+
severity: "warning",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Safely reads a file, returning null on error
|
|
121
|
+
*/
|
|
122
|
+
function safeReadFile(filePath) {
|
|
123
|
+
try {
|
|
124
|
+
return (0, node_fs_1.readFileSync)(filePath, "utf-8");
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Checks if file has a default export
|
|
132
|
+
*/
|
|
133
|
+
function hasDefaultExport(content) {
|
|
134
|
+
return content.includes("export default");
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Checks if file uses defineScreen
|
|
138
|
+
*/
|
|
139
|
+
function usesDefineScreen(content) {
|
|
140
|
+
return content.includes("defineScreen");
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Checks if file uses defineLayout
|
|
144
|
+
*/
|
|
145
|
+
function usesDefineLayout(content) {
|
|
146
|
+
return content.includes("defineLayout");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Checks if file has a param schema
|
|
150
|
+
*/
|
|
151
|
+
function hasParamSchema(content) {
|
|
152
|
+
return content.includes("paramsSchema") || content.includes("createParamSchema");
|
|
153
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File watcher for @teardown/navigation-metro
|
|
3
|
+
* Watches route files and triggers regeneration on changes
|
|
4
|
+
*/
|
|
5
|
+
import { type ValidationError } from "../validator/route-validator";
|
|
6
|
+
export interface WatcherOptions {
|
|
7
|
+
routesDir: string;
|
|
8
|
+
generatedDir: string;
|
|
9
|
+
prefixes: string[];
|
|
10
|
+
verbose: boolean;
|
|
11
|
+
onRegenerate?: () => void;
|
|
12
|
+
onError?: (errors: ValidationError[]) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Starts watching the routes directory for changes
|
|
16
|
+
* Returns a cleanup function to stop watching
|
|
17
|
+
*/
|
|
18
|
+
export declare function startRouteWatcher(options: WatcherOptions): () => void;
|
|
19
|
+
/**
|
|
20
|
+
* Stops the route watcher if it's running
|
|
21
|
+
*/
|
|
22
|
+
export declare function stopRouteWatcher(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Checks if the watcher is currently running
|
|
25
|
+
*/
|
|
26
|
+
export declare function isWatcherRunning(): boolean;
|
|
27
|
+
//# sourceMappingURL=file-watcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-watcher.d.ts","sourceRoot":"","sources":["../../src/watcher/file-watcher.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAAE,KAAK,eAAe,EAAkB,MAAM,8BAA8B,CAAC;AAEpF,MAAM,WAAW,cAAc;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CAC9C;AAID;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,IAAI,CAyFrE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAKvC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* File watcher for @teardown/navigation-metro
|
|
4
|
+
* Watches route files and triggers regeneration on changes
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.startRouteWatcher = startRouteWatcher;
|
|
8
|
+
exports.stopRouteWatcher = stopRouteWatcher;
|
|
9
|
+
exports.isWatcherRunning = isWatcherRunning;
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
11
|
+
const chokidar_1 = require("chokidar");
|
|
12
|
+
const route_generator_1 = require("../generator/route-generator");
|
|
13
|
+
const route_validator_1 = require("../validator/route-validator");
|
|
14
|
+
let watcherInstance = null;
|
|
15
|
+
/**
|
|
16
|
+
* Starts watching the routes directory for changes
|
|
17
|
+
* Returns a cleanup function to stop watching
|
|
18
|
+
*/
|
|
19
|
+
function startRouteWatcher(options) {
|
|
20
|
+
const { routesDir, generatedDir, prefixes, verbose, onRegenerate, onError } = options;
|
|
21
|
+
// Close existing watcher if any
|
|
22
|
+
if (watcherInstance) {
|
|
23
|
+
watcherInstance.close();
|
|
24
|
+
}
|
|
25
|
+
const watcher = (0, chokidar_1.watch)((0, node_path_1.join)(routesDir, "**/*.{ts,tsx}"), {
|
|
26
|
+
ignoreInitial: true,
|
|
27
|
+
ignored: [
|
|
28
|
+
/(^|[/\\])\../, // Dotfiles
|
|
29
|
+
/node_modules/,
|
|
30
|
+
/\.test\./,
|
|
31
|
+
/\.spec\./,
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
let debounceTimer = null;
|
|
35
|
+
const regenerate = () => {
|
|
36
|
+
if (debounceTimer) {
|
|
37
|
+
clearTimeout(debounceTimer);
|
|
38
|
+
}
|
|
39
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: debounced validate + generate
|
|
40
|
+
debounceTimer = setTimeout(() => {
|
|
41
|
+
try {
|
|
42
|
+
// Validate first
|
|
43
|
+
const errors = (0, route_validator_1.validateRoutes)(routesDir);
|
|
44
|
+
const hasErrors = errors.some((e) => e.severity === "error");
|
|
45
|
+
if (hasErrors) {
|
|
46
|
+
if (verbose) {
|
|
47
|
+
console.error("[teardown/navigation] Validation errors:");
|
|
48
|
+
for (const e of errors.filter((err) => err.severity === "error")) {
|
|
49
|
+
console.error(` ${e.file}: ${e.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
onError?.(errors);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Generate if validation passes
|
|
56
|
+
(0, route_generator_1.generateAllRouteFiles)({ routesDir, generatedDir, prefixes, verbose });
|
|
57
|
+
if (verbose) {
|
|
58
|
+
console.log("[teardown/navigation] Routes regenerated");
|
|
59
|
+
}
|
|
60
|
+
onRegenerate?.();
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (verbose) {
|
|
64
|
+
console.error("[teardown/navigation] Generation failed:", error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, 100);
|
|
68
|
+
};
|
|
69
|
+
watcher.on("add", (filePath) => {
|
|
70
|
+
if (verbose) {
|
|
71
|
+
console.log(`[teardown/navigation] File added: ${(0, node_path_1.relative)(routesDir, filePath)}`);
|
|
72
|
+
}
|
|
73
|
+
regenerate();
|
|
74
|
+
});
|
|
75
|
+
watcher.on("unlink", (filePath) => {
|
|
76
|
+
if (verbose) {
|
|
77
|
+
console.log(`[teardown/navigation] File removed: ${(0, node_path_1.relative)(routesDir, filePath)}`);
|
|
78
|
+
}
|
|
79
|
+
regenerate();
|
|
80
|
+
});
|
|
81
|
+
watcher.on("change", (filePath) => {
|
|
82
|
+
if (verbose) {
|
|
83
|
+
console.log(`[teardown/navigation] File changed: ${(0, node_path_1.relative)(routesDir, filePath)}`);
|
|
84
|
+
}
|
|
85
|
+
regenerate();
|
|
86
|
+
});
|
|
87
|
+
watcherInstance = watcher;
|
|
88
|
+
return () => {
|
|
89
|
+
if (debounceTimer) {
|
|
90
|
+
clearTimeout(debounceTimer);
|
|
91
|
+
}
|
|
92
|
+
watcher.close();
|
|
93
|
+
watcherInstance = null;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Stops the route watcher if it's running
|
|
98
|
+
*/
|
|
99
|
+
function stopRouteWatcher() {
|
|
100
|
+
if (watcherInstance) {
|
|
101
|
+
watcherInstance.close();
|
|
102
|
+
watcherInstance = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Checks if the watcher is currently running
|
|
107
|
+
*/
|
|
108
|
+
function isWatcherRunning() {
|
|
109
|
+
return watcherInstance !== null;
|
|
110
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/watcher/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Watcher module for @teardown/navigation-metro
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.stopRouteWatcher = exports.startRouteWatcher = exports.isWatcherRunning = void 0;
|
|
7
|
+
var file_watcher_1 = require("./file-watcher");
|
|
8
|
+
Object.defineProperty(exports, "isWatcherRunning", { enumerable: true, get: function () { return file_watcher_1.isWatcherRunning; } });
|
|
9
|
+
Object.defineProperty(exports, "startRouteWatcher", { enumerable: true, get: function () { return file_watcher_1.startRouteWatcher; } });
|
|
10
|
+
Object.defineProperty(exports, "stopRouteWatcher", { enumerable: true, get: function () { return file_watcher_1.stopRouteWatcher; } });
|
package/package.json
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teardown/navigation-metro",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.56",
|
|
4
4
|
"description": "Metro plugin for @teardown/navigation type generation",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
9
|
-
"type": "
|
|
10
|
-
"main": "./
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
13
|
-
"types": "./
|
|
14
|
-
"
|
|
15
|
-
"default": "./
|
|
16
|
-
}
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"require": "./dist/index.js",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
|
-
"
|
|
20
|
+
"dist/**/*"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"prepublishOnly": "bun run build",
|
|
22
25
|
"typecheck": "bun x tsgo --noEmit",
|
|
23
26
|
"lint": "bun x biome lint --write ./src",
|
|
24
27
|
"fmt": "bun x biome format --write ./src",
|
|
@@ -39,7 +42,7 @@
|
|
|
39
42
|
},
|
|
40
43
|
"devDependencies": {
|
|
41
44
|
"@biomejs/biome": "2.3.11",
|
|
42
|
-
"@teardown/tsconfig": "2.0.
|
|
45
|
+
"@teardown/tsconfig": "2.0.56",
|
|
43
46
|
"@types/node": "24.10.1",
|
|
44
47
|
"typescript": "5.9.3"
|
|
45
48
|
}
|