@teardown/navigation-metro 2.0.69 → 2.0.71
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/route-generator.d.ts +17 -0
- package/dist/generator/route-generator.d.ts.map +1 -1
- package/dist/generator/route-generator.js +276 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/templates/index.d.ts +5 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +15 -0
- package/dist/templates/route-templates.d.ts +41 -0
- package/dist/templates/route-templates.d.ts.map +1 -0
- package/dist/templates/route-templates.js +174 -0
- package/dist/watcher/file-watcher.d.ts +5 -0
- package/dist/watcher/file-watcher.d.ts.map +1 -1
- package/dist/watcher/file-watcher.js +26 -3
- package/package.json +2 -2
|
@@ -34,4 +34,21 @@ export declare function generateLinkingFileContent(routes: RouteNode[], prefixes
|
|
|
34
34
|
* Generates the register.d.ts file content
|
|
35
35
|
*/
|
|
36
36
|
export declare function generateRegisterFileContent(): string;
|
|
37
|
+
export interface RouteTreeEntry {
|
|
38
|
+
importName: string;
|
|
39
|
+
importPath: string;
|
|
40
|
+
path: string;
|
|
41
|
+
isLayout: boolean;
|
|
42
|
+
layoutType: string;
|
|
43
|
+
parentLayoutImportName: string | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Builds route tree entries for import generation
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildRouteTreeEntries(routes: RouteNode[], routesDir: string, generatedDir: string): RouteTreeEntry[];
|
|
49
|
+
/**
|
|
50
|
+
* Generates the routeTree.generated.ts file content
|
|
51
|
+
* Outputs a hierarchical navigator structure for nested navigation
|
|
52
|
+
*/
|
|
53
|
+
export declare function generateRouteTreeFileContent(routes: RouteNode[], routesDir: string, generatedDir: string): string;
|
|
37
54
|
//# sourceMappingURL=route-generator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-generator.d.ts","sourceRoot":"","sources":["../../src/generator/route-generator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAiB,KAAK,eAAe,EAAE,KAAK,SAAS,EAAuB,MAAM,yBAAyB,CAAC;AAEnH,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,eAAe,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"route-generator.d.ts","sourceRoot":"","sources":["../../src/generator/route-generator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAiB,KAAK,eAAe,EAAE,KAAK,SAAS,EAAuB,MAAM,yBAAyB,CAAC;AAEnH,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,eAAe,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAiCpE;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,eAAe,EAAE,CAwBhF;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,CAgDrE;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAkC1F;AAED;;GAEG;AACH,wBAAgB,2BAA2B,IAAI,MAAM,CAepD;AA4ED,MAAM,WAAW,cAAc;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED;;GAEG;AAEH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,cAAc,EAAE,CAqDpH;AA+JD;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CA4EjH"}
|
|
@@ -9,6 +9,8 @@ exports.buildRouteParamsInterface = buildRouteParamsInterface;
|
|
|
9
9
|
exports.generateRoutesFileContent = generateRoutesFileContent;
|
|
10
10
|
exports.generateLinkingFileContent = generateLinkingFileContent;
|
|
11
11
|
exports.generateRegisterFileContent = generateRegisterFileContent;
|
|
12
|
+
exports.buildRouteTreeEntries = buildRouteTreeEntries;
|
|
13
|
+
exports.generateRouteTreeFileContent = generateRouteTreeFileContent;
|
|
12
14
|
const node_fs_1 = require("node:fs");
|
|
13
15
|
const node_path_1 = require("node:path");
|
|
14
16
|
const file_scanner_1 = require("../scanner/file-scanner");
|
|
@@ -33,6 +35,8 @@ function generateAllRouteFiles(options) {
|
|
|
33
35
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(generatedDir, "register.d.ts"), registerContent);
|
|
34
36
|
const manifestContent = generateManifestContent(routes);
|
|
35
37
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(generatedDir, "manifest.json"), JSON.stringify(manifestContent, null, 2));
|
|
38
|
+
const routeTreeContent = generateRouteTreeFileContent(routes, routesDir, generatedDir);
|
|
39
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(generatedDir, "routeTree.generated.ts"), routeTreeContent);
|
|
36
40
|
if (verbose) {
|
|
37
41
|
const count = countRoutes(routes);
|
|
38
42
|
console.log(`[teardown/navigation] Generated ${count} routes`);
|
|
@@ -177,3 +181,275 @@ function generateManifestContent(routes) {
|
|
|
177
181
|
function countRoutes(routes) {
|
|
178
182
|
return (0, file_scanner_1.flattenRoutes)(routes).filter((r) => !r.isLayout).length;
|
|
179
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Converts a relative path to a valid import variable name
|
|
186
|
+
*/
|
|
187
|
+
function pathToImportName(relativePath) {
|
|
188
|
+
// Remove extension
|
|
189
|
+
const withoutExt = relativePath.replace(/\.(ts|tsx)$/, "");
|
|
190
|
+
// Convert to valid identifier
|
|
191
|
+
return withoutExt
|
|
192
|
+
.replace(/\//g, "_") // Replace slashes with underscores
|
|
193
|
+
.replace(/\[\.\.\.([^\]]+)\]/g, "CatchAll_$1") // [...slug] -> CatchAll_slug
|
|
194
|
+
.replace(/\[\[([^\]]+)\]\]/g, "Optional_$1") // [[id]] -> Optional_id
|
|
195
|
+
.replace(/\[([^\]]+)\]/g, "Param_$1") // [userId] -> Param_userId
|
|
196
|
+
.replace(/[^a-zA-Z0-9_]/g, "_") // Replace other special chars
|
|
197
|
+
.replace(/^_+/, "") // Remove leading underscores
|
|
198
|
+
.replace(/_+$/g, "") // Remove trailing underscores
|
|
199
|
+
.replace(/_+/g, "_"); // Collapse multiple underscores
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Generates the relative import path from generated dir to route file
|
|
203
|
+
*/
|
|
204
|
+
function getImportPath(routesDir, generatedDir, routeRelativePath) {
|
|
205
|
+
const routeAbsolutePath = (0, node_path_1.join)(routesDir, routeRelativePath);
|
|
206
|
+
const generatedAbsoluteDir = generatedDir;
|
|
207
|
+
// Get relative path from generated dir to route file
|
|
208
|
+
let importPath = (0, node_path_1.relative)(generatedAbsoluteDir, routeAbsolutePath);
|
|
209
|
+
// Remove extension for import
|
|
210
|
+
importPath = importPath.replace(/\.(ts|tsx)$/, "");
|
|
211
|
+
// Ensure it starts with ./ or ../
|
|
212
|
+
if (!importPath.startsWith(".")) {
|
|
213
|
+
importPath = `./${importPath}`;
|
|
214
|
+
}
|
|
215
|
+
return importPath;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Builds route tree entries for import generation
|
|
219
|
+
*/
|
|
220
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: route tree traversal and layout assignment
|
|
221
|
+
function buildRouteTreeEntries(routes, routesDir, generatedDir) {
|
|
222
|
+
const allRoutes = (0, file_scanner_1.flattenRoutes)(routes);
|
|
223
|
+
const entries = [];
|
|
224
|
+
// Create a map to find layouts by directory
|
|
225
|
+
const layoutsByDir = new Map();
|
|
226
|
+
for (const route of allRoutes) {
|
|
227
|
+
const importName = pathToImportName(route.relativePath);
|
|
228
|
+
const importPath = getImportPath(routesDir, generatedDir, route.relativePath);
|
|
229
|
+
const entry = {
|
|
230
|
+
importName,
|
|
231
|
+
importPath,
|
|
232
|
+
path: route.path,
|
|
233
|
+
isLayout: route.isLayout,
|
|
234
|
+
layoutType: route.layoutType,
|
|
235
|
+
parentLayoutImportName: null,
|
|
236
|
+
};
|
|
237
|
+
entries.push(entry);
|
|
238
|
+
if (route.isLayout) {
|
|
239
|
+
// Store layout by its directory
|
|
240
|
+
const layoutDir = (0, node_path_1.dirname)(route.relativePath);
|
|
241
|
+
layoutsByDir.set(layoutDir === "." ? "" : layoutDir, entry);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Second pass: assign parent layouts
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
if (!entry.isLayout) {
|
|
247
|
+
// Find the relative path's directory
|
|
248
|
+
const routePath = allRoutes.find((r) => pathToImportName(r.relativePath) === entry.importName);
|
|
249
|
+
if (routePath) {
|
|
250
|
+
const routeDir = (0, node_path_1.dirname)(routePath.relativePath);
|
|
251
|
+
// Look for layout in same directory or parent directories
|
|
252
|
+
let currentDir = routeDir === "." ? "" : routeDir;
|
|
253
|
+
while (currentDir !== undefined) {
|
|
254
|
+
const layout = layoutsByDir.get(currentDir);
|
|
255
|
+
if (layout) {
|
|
256
|
+
entry.parentLayoutImportName = layout.importName;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
if (currentDir === "")
|
|
260
|
+
break;
|
|
261
|
+
currentDir = (0, node_path_1.dirname)(currentDir);
|
|
262
|
+
if (currentDir === ".")
|
|
263
|
+
currentDir = "";
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return entries;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Builds a hierarchical navigator structure from route nodes
|
|
272
|
+
*/
|
|
273
|
+
function buildNavigatorHierarchy(routes, routesDir, generatedDir) {
|
|
274
|
+
const allImports = [];
|
|
275
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: builds nested navigator hierarchy
|
|
276
|
+
function processNode(nodes, parentName) {
|
|
277
|
+
// Find layout in this group
|
|
278
|
+
const layoutNode = nodes.find((n) => n.isLayout);
|
|
279
|
+
// Default to stack if no layout or layout type is "none"
|
|
280
|
+
const layoutType = layoutNode?.layoutType && layoutNode.layoutType !== "none" ? layoutNode.layoutType : "stack";
|
|
281
|
+
const layoutImportName = layoutNode ? pathToImportName(layoutNode.relativePath) : null;
|
|
282
|
+
if (layoutNode) {
|
|
283
|
+
allImports.push({
|
|
284
|
+
name: pathToImportName(layoutNode.relativePath),
|
|
285
|
+
path: getImportPath(routesDir, generatedDir, layoutNode.relativePath),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const navigatorInfo = {
|
|
289
|
+
name: parentName,
|
|
290
|
+
type: layoutType,
|
|
291
|
+
layoutImportName,
|
|
292
|
+
screens: [],
|
|
293
|
+
nestedNavigators: [],
|
|
294
|
+
};
|
|
295
|
+
// Process children
|
|
296
|
+
for (const node of nodes) {
|
|
297
|
+
if (node.isLayout)
|
|
298
|
+
continue;
|
|
299
|
+
const importName = pathToImportName(node.relativePath);
|
|
300
|
+
const importPath = getImportPath(routesDir, generatedDir, node.relativePath);
|
|
301
|
+
allImports.push({ name: importName, path: importPath });
|
|
302
|
+
// Check if this node has children with a layout (nested navigator)
|
|
303
|
+
const childLayout = node.children.find((c) => c.isLayout);
|
|
304
|
+
if (childLayout) {
|
|
305
|
+
// This is a nested navigator
|
|
306
|
+
const nestedNav = processNode(node.children, importName);
|
|
307
|
+
navigatorInfo.nestedNavigators.push(nestedNav);
|
|
308
|
+
}
|
|
309
|
+
else if (node.children.length > 0) {
|
|
310
|
+
// Has children but no layout - process as screens in current navigator
|
|
311
|
+
navigatorInfo.screens.push({
|
|
312
|
+
name: node.name.split("/").pop() || node.name,
|
|
313
|
+
importName,
|
|
314
|
+
path: node.path,
|
|
315
|
+
});
|
|
316
|
+
// Recursively add children as screens
|
|
317
|
+
const childScreens = collectScreens(node.children, routesDir, generatedDir, allImports);
|
|
318
|
+
navigatorInfo.screens.push(...childScreens);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
// Regular screen
|
|
322
|
+
navigatorInfo.screens.push({
|
|
323
|
+
name: node.name.split("/").pop() || node.name,
|
|
324
|
+
importName,
|
|
325
|
+
path: node.path,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return navigatorInfo;
|
|
330
|
+
}
|
|
331
|
+
function collectScreens(nodes, rDir, gDir, imports) {
|
|
332
|
+
const screens = [];
|
|
333
|
+
for (const node of nodes) {
|
|
334
|
+
if (node.isLayout)
|
|
335
|
+
continue;
|
|
336
|
+
const importName = pathToImportName(node.relativePath);
|
|
337
|
+
const importPath = getImportPath(rDir, gDir, node.relativePath);
|
|
338
|
+
imports.push({ name: importName, path: importPath });
|
|
339
|
+
screens.push({
|
|
340
|
+
name: node.name.split("/").pop() || node.name,
|
|
341
|
+
importName,
|
|
342
|
+
path: node.path,
|
|
343
|
+
});
|
|
344
|
+
if (node.children.length > 0) {
|
|
345
|
+
screens.push(...collectScreens(node.children, rDir, gDir, imports));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return screens;
|
|
349
|
+
}
|
|
350
|
+
const rootNavigator = processNode(routes, "_root");
|
|
351
|
+
return { rootNavigator, allImports };
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Generates the navigator tree as TypeScript code
|
|
355
|
+
*/
|
|
356
|
+
function generateNavigatorCode(nav, indent) {
|
|
357
|
+
const lines = [];
|
|
358
|
+
lines.push(`${indent}{`);
|
|
359
|
+
lines.push(`${indent}\ttype: "${nav.type}",`);
|
|
360
|
+
if (nav.layoutImportName) {
|
|
361
|
+
lines.push(`${indent}\tlayout: ${nav.layoutImportName},`);
|
|
362
|
+
}
|
|
363
|
+
// Screens
|
|
364
|
+
lines.push(`${indent}\tscreens: {`);
|
|
365
|
+
for (const screen of nav.screens) {
|
|
366
|
+
lines.push(`${indent}\t\t"${screen.name}": {`);
|
|
367
|
+
lines.push(`${indent}\t\t\tscreen: ${screen.importName},`);
|
|
368
|
+
lines.push(`${indent}\t\t\tpath: "${screen.path}",`);
|
|
369
|
+
lines.push(`${indent}\t\t},`);
|
|
370
|
+
}
|
|
371
|
+
lines.push(`${indent}\t},`);
|
|
372
|
+
// Nested navigators
|
|
373
|
+
if (nav.nestedNavigators.length > 0) {
|
|
374
|
+
lines.push(`${indent}\tnavigators: {`);
|
|
375
|
+
for (const nestedNav of nav.nestedNavigators) {
|
|
376
|
+
lines.push(`${indent}\t\t"${nestedNav.name}":`);
|
|
377
|
+
const nestedLines = generateNavigatorCode(nestedNav, `${indent}\t\t`);
|
|
378
|
+
lines.push(...nestedLines.map((l) => l.replace(/^/, "")));
|
|
379
|
+
lines[lines.length - 1] += ",";
|
|
380
|
+
}
|
|
381
|
+
lines.push(`${indent}\t},`);
|
|
382
|
+
}
|
|
383
|
+
lines.push(`${indent}}`);
|
|
384
|
+
return lines;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Generates the routeTree.generated.ts file content
|
|
388
|
+
* Outputs a hierarchical navigator structure for nested navigation
|
|
389
|
+
*/
|
|
390
|
+
function generateRouteTreeFileContent(routes, routesDir, generatedDir) {
|
|
391
|
+
const { rootNavigator, allImports } = buildNavigatorHierarchy(routes, routesDir, generatedDir);
|
|
392
|
+
const lines = [
|
|
393
|
+
"/* eslint-disable */",
|
|
394
|
+
"// @ts-nocheck",
|
|
395
|
+
"",
|
|
396
|
+
"// Auto-generated by @teardown/navigation",
|
|
397
|
+
"// Do not edit this file directly",
|
|
398
|
+
`// Generated at: ${new Date().toISOString()}`,
|
|
399
|
+
"",
|
|
400
|
+
'import type { NavigatorNode } from "@teardown/navigation";',
|
|
401
|
+
"",
|
|
402
|
+
];
|
|
403
|
+
// Generate unique imports
|
|
404
|
+
const uniqueImports = new Map();
|
|
405
|
+
for (const imp of allImports) {
|
|
406
|
+
if (!uniqueImports.has(imp.name)) {
|
|
407
|
+
uniqueImports.set(imp.name, imp.path);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
for (const [name, path] of uniqueImports) {
|
|
411
|
+
lines.push(`import ${name} from "${path}";`);
|
|
412
|
+
}
|
|
413
|
+
lines.push("");
|
|
414
|
+
// Generate the hierarchical routeTree
|
|
415
|
+
lines.push("export const routeTree: NavigatorNode =");
|
|
416
|
+
const treeLines = generateNavigatorCode(rootNavigator, "");
|
|
417
|
+
lines.push(...treeLines);
|
|
418
|
+
lines[lines.length - 1] += ";";
|
|
419
|
+
lines.push("");
|
|
420
|
+
// Also export flat screens for backwards compatibility
|
|
421
|
+
const entries = buildRouteTreeEntries(routes, routesDir, generatedDir);
|
|
422
|
+
const screenEntries = entries.filter((e) => !e.isLayout);
|
|
423
|
+
lines.push("// Flat screens export for backwards compatibility");
|
|
424
|
+
lines.push("export const screens = {");
|
|
425
|
+
for (const entry of screenEntries) {
|
|
426
|
+
lines.push(`\t"${entry.path}": ${entry.importName},`);
|
|
427
|
+
}
|
|
428
|
+
lines.push("} as const;");
|
|
429
|
+
lines.push("");
|
|
430
|
+
// Export layouts
|
|
431
|
+
const layoutEntries = entries.filter((e) => e.isLayout);
|
|
432
|
+
lines.push("export const layouts = {");
|
|
433
|
+
for (const entry of layoutEntries) {
|
|
434
|
+
const layoutDir = (0, node_path_1.dirname)(entry.importPath.replace(/^\.\//, "").replace(/^\.\.\//, ""));
|
|
435
|
+
const layoutKey = layoutDir === "." ? "_root" : layoutDir.replace(/\//g, "_");
|
|
436
|
+
lines.push(`\t"${layoutKey}": ${entry.importName},`);
|
|
437
|
+
}
|
|
438
|
+
lines.push("} as const;");
|
|
439
|
+
lines.push("");
|
|
440
|
+
// Export types
|
|
441
|
+
lines.push("export type Screens = typeof screens;");
|
|
442
|
+
lines.push("export type Layouts = typeof layouts;");
|
|
443
|
+
lines.push("export type RouteTreeType = typeof routeTree;");
|
|
444
|
+
lines.push("");
|
|
445
|
+
// Export route paths array
|
|
446
|
+
lines.push("export const routePaths = [");
|
|
447
|
+
for (const entry of screenEntries) {
|
|
448
|
+
lines.push(`\t"${entry.path}",`);
|
|
449
|
+
}
|
|
450
|
+
lines.push("] as const;");
|
|
451
|
+
lines.push("");
|
|
452
|
+
lines.push("export type RoutePath = (typeof routePaths)[number];");
|
|
453
|
+
lines.push("");
|
|
454
|
+
return lines.join("\n");
|
|
455
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,11 @@ export interface TeardownNavigationOptions {
|
|
|
26
26
|
* @default false
|
|
27
27
|
*/
|
|
28
28
|
verbose?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Auto-populate new route files with template content
|
|
31
|
+
* @default true
|
|
32
|
+
*/
|
|
33
|
+
autoTemplate?: boolean;
|
|
29
34
|
}
|
|
30
35
|
/**
|
|
31
36
|
* Metro configuration type (simplified)
|
|
@@ -76,6 +81,7 @@ export type { GenerateOptions, RouteParamEntry } from "./generator/route-generat
|
|
|
76
81
|
export { generateAllRouteFiles } from "./generator/route-generator";
|
|
77
82
|
export type { ParamDefinition, RouteNode, ScanError, ScanResult } from "./scanner/file-scanner";
|
|
78
83
|
export { buildUrlPath, extractParams, filePathToScreenName, flattenRoutes, scanRoutesDirectory, } from "./scanner/file-scanner";
|
|
84
|
+
export { extractParamsFromPath, fileNameToScreenName, generateLayoutTemplate, generateRouteTemplate, generateScreenTemplate, getTemplateType, isFileEmpty, screenNameToTitle, } from "./templates/route-templates";
|
|
79
85
|
export type { ValidationError } from "./validator/route-validator";
|
|
80
86
|
export { validateRoutes } from "./validator/route-validator";
|
|
81
87
|
export type { WatcherOptions } from "./watcher/file-watcher";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAmCH;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACzC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IAEpB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAmCH;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACzC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IAEpB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE;QACV,cAAc,CAAC,EAAE,CAChB,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,KACnB;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAC;QAC/C,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACvB,CAAC;IACF,WAAW,CAAC,EAAE;QACb,4BAA4B,CAAC,EAAE,OAAO,CAAC;QACvC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACvB,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,yBAA8B,GAAG,WAAW,CAsEhH;AAED,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAEpF,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAChG,OAAO,EACN,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,aAAa,EACb,mBAAmB,GACnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACN,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACtB,eAAe,EACf,WAAW,EACX,iBAAiB,GACjB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Metro plugin for type-safe file-based navigation
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.stopRouteWatcher = exports.startRouteWatcher = exports.isWatcherRunning = exports.validateRoutes = exports.scanRoutesDirectory = exports.flattenRoutes = exports.filePathToScreenName = exports.extractParams = exports.buildUrlPath = exports.generateAllRouteFiles = void 0;
|
|
7
|
+
exports.stopRouteWatcher = exports.startRouteWatcher = exports.isWatcherRunning = exports.validateRoutes = exports.screenNameToTitle = exports.isFileEmpty = exports.getTemplateType = exports.generateScreenTemplate = exports.generateRouteTemplate = exports.generateLayoutTemplate = exports.fileNameToScreenName = exports.extractParamsFromPath = exports.scanRoutesDirectory = exports.flattenRoutes = exports.filePathToScreenName = exports.extractParams = exports.buildUrlPath = exports.generateAllRouteFiles = void 0;
|
|
8
8
|
exports.withTeardownNavigation = withTeardownNavigation;
|
|
9
9
|
const node_fs_1 = require("node:fs");
|
|
10
10
|
const node_path_1 = require("node:path");
|
|
@@ -62,7 +62,7 @@ function readSlugFromConfig(projectRoot) {
|
|
|
62
62
|
* ```
|
|
63
63
|
*/
|
|
64
64
|
function withTeardownNavigation(config, options = {}) {
|
|
65
|
-
const { routesDir = "./src/routes", generatedDir = "./.teardown", verbose = false } = options;
|
|
65
|
+
const { routesDir = "./src/routes", generatedDir = "./.teardown", verbose = false, autoTemplate = true } = options;
|
|
66
66
|
const projectRoot = config.projectRoot ?? process.cwd();
|
|
67
67
|
const absoluteRoutesDir = (0, node_path_1.resolve)(projectRoot, routesDir);
|
|
68
68
|
const absoluteGeneratedDir = (0, node_path_1.resolve)(projectRoot, generatedDir);
|
|
@@ -99,6 +99,7 @@ function withTeardownNavigation(config, options = {}) {
|
|
|
99
99
|
generatedDir: absoluteGeneratedDir,
|
|
100
100
|
prefixes,
|
|
101
101
|
verbose,
|
|
102
|
+
autoTemplate,
|
|
102
103
|
onRegenerate: () => {
|
|
103
104
|
if (verbose) {
|
|
104
105
|
console.log("[teardown/navigation] Routes regenerated");
|
|
@@ -134,6 +135,15 @@ Object.defineProperty(exports, "extractParams", { enumerable: true, get: functio
|
|
|
134
135
|
Object.defineProperty(exports, "filePathToScreenName", { enumerable: true, get: function () { return file_scanner_1.filePathToScreenName; } });
|
|
135
136
|
Object.defineProperty(exports, "flattenRoutes", { enumerable: true, get: function () { return file_scanner_1.flattenRoutes; } });
|
|
136
137
|
Object.defineProperty(exports, "scanRoutesDirectory", { enumerable: true, get: function () { return file_scanner_1.scanRoutesDirectory; } });
|
|
138
|
+
var route_templates_1 = require("./templates/route-templates");
|
|
139
|
+
Object.defineProperty(exports, "extractParamsFromPath", { enumerable: true, get: function () { return route_templates_1.extractParamsFromPath; } });
|
|
140
|
+
Object.defineProperty(exports, "fileNameToScreenName", { enumerable: true, get: function () { return route_templates_1.fileNameToScreenName; } });
|
|
141
|
+
Object.defineProperty(exports, "generateLayoutTemplate", { enumerable: true, get: function () { return route_templates_1.generateLayoutTemplate; } });
|
|
142
|
+
Object.defineProperty(exports, "generateRouteTemplate", { enumerable: true, get: function () { return route_templates_1.generateRouteTemplate; } });
|
|
143
|
+
Object.defineProperty(exports, "generateScreenTemplate", { enumerable: true, get: function () { return route_templates_1.generateScreenTemplate; } });
|
|
144
|
+
Object.defineProperty(exports, "getTemplateType", { enumerable: true, get: function () { return route_templates_1.getTemplateType; } });
|
|
145
|
+
Object.defineProperty(exports, "isFileEmpty", { enumerable: true, get: function () { return route_templates_1.isFileEmpty; } });
|
|
146
|
+
Object.defineProperty(exports, "screenNameToTitle", { enumerable: true, get: function () { return route_templates_1.screenNameToTitle; } });
|
|
137
147
|
var route_validator_1 = require("./validator/route-validator");
|
|
138
148
|
Object.defineProperty(exports, "validateRoutes", { enumerable: true, get: function () { return route_validator_1.validateRoutes; } });
|
|
139
149
|
var file_watcher_2 = require("./watcher/file-watcher");
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template module exports for @teardown/navigation-metro
|
|
3
|
+
*/
|
|
4
|
+
export { extractParamsFromPath, fileNameToScreenName, generateLayoutTemplate, generateRouteTemplate, generateScreenTemplate, getTemplateType, isFileEmpty, screenNameToTitle, } from "./route-templates";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACtB,eAAe,EACf,WAAW,EACX,iBAAiB,GACjB,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Template module exports for @teardown/navigation-metro
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.screenNameToTitle = exports.isFileEmpty = exports.getTemplateType = exports.generateScreenTemplate = exports.generateRouteTemplate = exports.generateLayoutTemplate = exports.fileNameToScreenName = exports.extractParamsFromPath = void 0;
|
|
7
|
+
var route_templates_1 = require("./route-templates");
|
|
8
|
+
Object.defineProperty(exports, "extractParamsFromPath", { enumerable: true, get: function () { return route_templates_1.extractParamsFromPath; } });
|
|
9
|
+
Object.defineProperty(exports, "fileNameToScreenName", { enumerable: true, get: function () { return route_templates_1.fileNameToScreenName; } });
|
|
10
|
+
Object.defineProperty(exports, "generateLayoutTemplate", { enumerable: true, get: function () { return route_templates_1.generateLayoutTemplate; } });
|
|
11
|
+
Object.defineProperty(exports, "generateRouteTemplate", { enumerable: true, get: function () { return route_templates_1.generateRouteTemplate; } });
|
|
12
|
+
Object.defineProperty(exports, "generateScreenTemplate", { enumerable: true, get: function () { return route_templates_1.generateScreenTemplate; } });
|
|
13
|
+
Object.defineProperty(exports, "getTemplateType", { enumerable: true, get: function () { return route_templates_1.getTemplateType; } });
|
|
14
|
+
Object.defineProperty(exports, "isFileEmpty", { enumerable: true, get: function () { return route_templates_1.isFileEmpty; } });
|
|
15
|
+
Object.defineProperty(exports, "screenNameToTitle", { enumerable: true, get: function () { return route_templates_1.screenNameToTitle; } });
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route templates for @teardown/navigation-metro
|
|
3
|
+
* Generates template content for new route files
|
|
4
|
+
*/
|
|
5
|
+
import type { ParamDefinition } from "../scanner/file-scanner";
|
|
6
|
+
/**
|
|
7
|
+
* Checks if a file is empty or has minimal content (less than 10 bytes)
|
|
8
|
+
*/
|
|
9
|
+
export declare function isFileEmpty(filePath: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Converts a filename to a PascalCase screen name
|
|
12
|
+
* e.g., "user-profile" -> "UserProfile", "[userId]" -> "UserId"
|
|
13
|
+
*/
|
|
14
|
+
export declare function fileNameToScreenName(fileName: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Converts a PascalCase name to a human-readable title
|
|
17
|
+
* e.g., "UserProfile" -> "User Profile"
|
|
18
|
+
*/
|
|
19
|
+
export declare function screenNameToTitle(screenName: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Generates the template content for a screen file
|
|
22
|
+
*/
|
|
23
|
+
export declare function generateScreenTemplate(routeName: string, params?: ParamDefinition[]): string;
|
|
24
|
+
/**
|
|
25
|
+
* Generates the template content for a layout file
|
|
26
|
+
*/
|
|
27
|
+
export declare function generateLayoutTemplate(layoutType?: "stack" | "tabs" | "drawer"): string;
|
|
28
|
+
/**
|
|
29
|
+
* Determines the appropriate template type from a file path
|
|
30
|
+
*/
|
|
31
|
+
export declare function getTemplateType(relativePath: string): "screen" | "layout";
|
|
32
|
+
/**
|
|
33
|
+
* Extracts dynamic parameters from a filename
|
|
34
|
+
* Mirrors the logic from file-scanner.ts
|
|
35
|
+
*/
|
|
36
|
+
export declare function extractParamsFromPath(fileName: string): ParamDefinition[];
|
|
37
|
+
/**
|
|
38
|
+
* Generates the appropriate template for a route file
|
|
39
|
+
*/
|
|
40
|
+
export declare function generateRouteTemplate(relativePath: string): string;
|
|
41
|
+
//# sourceMappingURL=route-templates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-templates.d.ts","sourceRoot":"","sources":["../../src/templates/route-templates.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE/D;;GAEG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAarD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgB7D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,GAAE,eAAe,EAAO,GAAG,MAAM,CA2ChG;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,GAAE,OAAO,GAAG,MAAM,GAAG,QAAkB,GAAG,MAAM,CA0BhG;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAIzE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,EAAE,CAoBzE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CASlE"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Route templates for @teardown/navigation-metro
|
|
4
|
+
* Generates template content for new route files
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.isFileEmpty = isFileEmpty;
|
|
8
|
+
exports.fileNameToScreenName = fileNameToScreenName;
|
|
9
|
+
exports.screenNameToTitle = screenNameToTitle;
|
|
10
|
+
exports.generateScreenTemplate = generateScreenTemplate;
|
|
11
|
+
exports.generateLayoutTemplate = generateLayoutTemplate;
|
|
12
|
+
exports.getTemplateType = getTemplateType;
|
|
13
|
+
exports.extractParamsFromPath = extractParamsFromPath;
|
|
14
|
+
exports.generateRouteTemplate = generateRouteTemplate;
|
|
15
|
+
const node_fs_1 = require("node:fs");
|
|
16
|
+
const node_path_1 = require("node:path");
|
|
17
|
+
/**
|
|
18
|
+
* Checks if a file is empty or has minimal content (less than 10 bytes)
|
|
19
|
+
*/
|
|
20
|
+
function isFileEmpty(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
const stats = (0, node_fs_1.statSync)(filePath);
|
|
23
|
+
if (stats.size < 10) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
// Also check if file only contains whitespace
|
|
27
|
+
const content = (0, node_fs_1.readFileSync)(filePath, "utf-8").trim();
|
|
28
|
+
return content.length === 0;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// If we can't read the file, assume it's not empty
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Converts a filename to a PascalCase screen name
|
|
37
|
+
* e.g., "user-profile" -> "UserProfile", "[userId]" -> "UserId"
|
|
38
|
+
*/
|
|
39
|
+
function fileNameToScreenName(fileName) {
|
|
40
|
+
const ext = (0, node_path_1.extname)(fileName);
|
|
41
|
+
const baseName = (0, node_path_1.basename)(fileName, ext);
|
|
42
|
+
// Remove param brackets and special characters
|
|
43
|
+
const cleanName = baseName
|
|
44
|
+
.replace(/\[\.\.\.([^\]]+)\]/g, "$1") // [...slug] -> slug
|
|
45
|
+
.replace(/\[\[([^\]]+)\]\]/g, "$1") // [[id]] -> id
|
|
46
|
+
.replace(/\[([^\]]+)\]/g, "$1") // [userId] -> userId
|
|
47
|
+
.replace(/\?/g, ""); // Remove optional markers
|
|
48
|
+
// Convert to PascalCase
|
|
49
|
+
return cleanName
|
|
50
|
+
.split(/[-_]/)
|
|
51
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
52
|
+
.join("");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Converts a PascalCase name to a human-readable title
|
|
56
|
+
* e.g., "UserProfile" -> "User Profile"
|
|
57
|
+
*/
|
|
58
|
+
function screenNameToTitle(screenName) {
|
|
59
|
+
return screenName.replace(/([A-Z])/g, " $1").trim();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Generates the template content for a screen file
|
|
63
|
+
*/
|
|
64
|
+
function generateScreenTemplate(routeName, params = []) {
|
|
65
|
+
const screenName = fileNameToScreenName(routeName);
|
|
66
|
+
const title = screenNameToTitle(screenName);
|
|
67
|
+
// Build params type annotation if there are params
|
|
68
|
+
let paramsImport = "";
|
|
69
|
+
let paramsType = "";
|
|
70
|
+
let paramsUsage = "";
|
|
71
|
+
if (params.length > 0) {
|
|
72
|
+
const paramEntries = params.map((p) => {
|
|
73
|
+
const type = p.isCatchAll ? "string[]" : "string";
|
|
74
|
+
return `${p.name}${p.isOptional ? "?" : ""}: ${type}`;
|
|
75
|
+
});
|
|
76
|
+
paramsType = `{ ${paramEntries.join("; ")} }`;
|
|
77
|
+
paramsImport = '\nimport { useTypedParams } from "@teardown/navigation";';
|
|
78
|
+
paramsUsage = `
|
|
79
|
+
const params = useTypedParams<${paramsType}>();`;
|
|
80
|
+
}
|
|
81
|
+
return `/**
|
|
82
|
+
* ${title} Screen
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
import { defineScreen } from "@teardown/navigation/primitives";${paramsImport}
|
|
86
|
+
import type React from "react";
|
|
87
|
+
import { View, Text } from "react-native";
|
|
88
|
+
|
|
89
|
+
function ${screenName}Screen(): React.JSX.Element {${paramsUsage}
|
|
90
|
+
return (
|
|
91
|
+
<View className="flex-1 bg-background p-6">
|
|
92
|
+
<Text className="text-foreground">${title}</Text>
|
|
93
|
+
</View>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default defineScreen({
|
|
98
|
+
component: ${screenName}Screen,
|
|
99
|
+
options: {
|
|
100
|
+
title: "${title}",
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Generates the template content for a layout file
|
|
107
|
+
*/
|
|
108
|
+
function generateLayoutTemplate(layoutType = "stack") {
|
|
109
|
+
const screenOptionsMap = {
|
|
110
|
+
stack: `{
|
|
111
|
+
headerShown: true,
|
|
112
|
+
}`,
|
|
113
|
+
tabs: `{
|
|
114
|
+
headerShown: false,
|
|
115
|
+
tabBarActiveTintColor: "#007AFF",
|
|
116
|
+
}`,
|
|
117
|
+
drawer: `{
|
|
118
|
+
headerShown: true,
|
|
119
|
+
drawerType: "front",
|
|
120
|
+
}`,
|
|
121
|
+
};
|
|
122
|
+
return `/**
|
|
123
|
+
* Layout Configuration
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
import { defineLayout } from "@teardown/navigation/primitives";
|
|
127
|
+
|
|
128
|
+
export default defineLayout({
|
|
129
|
+
type: "${layoutType}",
|
|
130
|
+
screenOptions: ${screenOptionsMap[layoutType]},
|
|
131
|
+
});
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Determines the appropriate template type from a file path
|
|
136
|
+
*/
|
|
137
|
+
function getTemplateType(relativePath) {
|
|
138
|
+
const ext = (0, node_path_1.extname)(relativePath);
|
|
139
|
+
const fileName = (0, node_path_1.basename)(relativePath, ext);
|
|
140
|
+
return fileName === "_layout" ? "layout" : "screen";
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Extracts dynamic parameters from a filename
|
|
144
|
+
* Mirrors the logic from file-scanner.ts
|
|
145
|
+
*/
|
|
146
|
+
function extractParamsFromPath(fileName) {
|
|
147
|
+
const params = [];
|
|
148
|
+
// Match patterns: [param], [param]?, [...param], [[param]]
|
|
149
|
+
const regex = /\[\[([^\]]+)\]\]|\[(?:\.\.\.)?([^\]]+)\]\??/g;
|
|
150
|
+
let match;
|
|
151
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex iteration pattern
|
|
152
|
+
while ((match = regex.exec(fileName)) !== null) {
|
|
153
|
+
const fullMatch = match[0];
|
|
154
|
+
// match[1] is for [[param]] syntax, match[2] is for [param] syntax
|
|
155
|
+
const paramName = (match[1] || match[2]).replace("?", "");
|
|
156
|
+
params.push({
|
|
157
|
+
name: paramName,
|
|
158
|
+
isOptional: fullMatch.endsWith("?") || fullMatch.startsWith("[["),
|
|
159
|
+
isCatchAll: fullMatch.startsWith("[..."),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return params;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Generates the appropriate template for a route file
|
|
166
|
+
*/
|
|
167
|
+
function generateRouteTemplate(relativePath) {
|
|
168
|
+
const templateType = getTemplateType(relativePath);
|
|
169
|
+
if (templateType === "layout") {
|
|
170
|
+
return generateLayoutTemplate("stack");
|
|
171
|
+
}
|
|
172
|
+
const params = extractParamsFromPath(relativePath);
|
|
173
|
+
return generateScreenTemplate(relativePath, params);
|
|
174
|
+
}
|
|
@@ -8,6 +8,11 @@ export interface WatcherOptions {
|
|
|
8
8
|
generatedDir: string;
|
|
9
9
|
prefixes: string[];
|
|
10
10
|
verbose: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Auto-populate new route files with template content
|
|
13
|
+
* @default true
|
|
14
|
+
*/
|
|
15
|
+
autoTemplate?: boolean;
|
|
11
16
|
onRegenerate?: () => void;
|
|
12
17
|
onError?: (errors: ValidationError[]) => void;
|
|
13
18
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-watcher.d.ts","sourceRoot":"","sources":["../../src/watcher/file-watcher.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"file-watcher.d.ts","sourceRoot":"","sources":["../../src/watcher/file-watcher.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,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;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,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,CA+GrE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAKvC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C"}
|
|
@@ -7,9 +7,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.startRouteWatcher = startRouteWatcher;
|
|
8
8
|
exports.stopRouteWatcher = stopRouteWatcher;
|
|
9
9
|
exports.isWatcherRunning = isWatcherRunning;
|
|
10
|
+
const node_fs_1 = require("node:fs");
|
|
10
11
|
const node_path_1 = require("node:path");
|
|
11
12
|
const chokidar_1 = require("chokidar");
|
|
12
13
|
const route_generator_1 = require("../generator/route-generator");
|
|
14
|
+
const route_templates_1 = require("../templates/route-templates");
|
|
13
15
|
const route_validator_1 = require("../validator/route-validator");
|
|
14
16
|
let watcherInstance = null;
|
|
15
17
|
/**
|
|
@@ -17,7 +19,7 @@ let watcherInstance = null;
|
|
|
17
19
|
* Returns a cleanup function to stop watching
|
|
18
20
|
*/
|
|
19
21
|
function startRouteWatcher(options) {
|
|
20
|
-
const { routesDir, generatedDir, prefixes, verbose, onRegenerate, onError } = options;
|
|
22
|
+
const { routesDir, generatedDir, prefixes, verbose, autoTemplate = true, onRegenerate, onError } = options;
|
|
21
23
|
// Close existing watcher if any
|
|
22
24
|
if (watcherInstance) {
|
|
23
25
|
watcherInstance.close();
|
|
@@ -67,10 +69,31 @@ function startRouteWatcher(options) {
|
|
|
67
69
|
}, 100);
|
|
68
70
|
};
|
|
69
71
|
watcher.on("add", (filePath) => {
|
|
72
|
+
const relativePath = (0, node_path_1.relative)(routesDir, filePath);
|
|
70
73
|
if (verbose) {
|
|
71
|
-
console.log(`[teardown/navigation] File added: ${
|
|
74
|
+
console.log(`[teardown/navigation] File added: ${relativePath}`);
|
|
72
75
|
}
|
|
73
|
-
|
|
76
|
+
// Add delay to let filesystem settle before checking if file is empty
|
|
77
|
+
// This prevents race conditions where the file hasn't finished being created
|
|
78
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: template generation with error handling
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
// Auto-populate empty files with template content
|
|
81
|
+
if (autoTemplate && (0, route_templates_1.isFileEmpty)(filePath)) {
|
|
82
|
+
try {
|
|
83
|
+
const templateContent = (0, route_templates_1.generateRouteTemplate)(relativePath);
|
|
84
|
+
(0, node_fs_1.writeFileSync)(filePath, templateContent);
|
|
85
|
+
if (verbose) {
|
|
86
|
+
console.log(`[teardown/navigation] Generated template for: ${relativePath}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (verbose) {
|
|
91
|
+
console.error(`[teardown/navigation] Failed to generate template for ${relativePath}:`, error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
regenerate();
|
|
96
|
+
}, 50);
|
|
74
97
|
});
|
|
75
98
|
watcher.on("unlink", (filePath) => {
|
|
76
99
|
if (verbose) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teardown/navigation-metro",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.71",
|
|
4
4
|
"description": "Metro plugin for @teardown/navigation type generation",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@biomejs/biome": "2.3.11",
|
|
45
|
-
"@teardown/tsconfig": "2.0.
|
|
45
|
+
"@teardown/tsconfig": "2.0.71",
|
|
46
46
|
"@types/node": "24.10.1",
|
|
47
47
|
"typescript": "5.9.3"
|
|
48
48
|
}
|