tokenlean 0.1.0
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/README.md +248 -0
- package/bin/tl-api.mjs +515 -0
- package/bin/tl-blame.mjs +345 -0
- package/bin/tl-complexity.mjs +514 -0
- package/bin/tl-component.mjs +274 -0
- package/bin/tl-config.mjs +135 -0
- package/bin/tl-context.mjs +156 -0
- package/bin/tl-coverage.mjs +456 -0
- package/bin/tl-deps.mjs +474 -0
- package/bin/tl-diff.mjs +183 -0
- package/bin/tl-entry.mjs +256 -0
- package/bin/tl-env.mjs +376 -0
- package/bin/tl-exports.mjs +583 -0
- package/bin/tl-flow.mjs +324 -0
- package/bin/tl-history.mjs +289 -0
- package/bin/tl-hotspots.mjs +321 -0
- package/bin/tl-impact.mjs +345 -0
- package/bin/tl-prompt.mjs +175 -0
- package/bin/tl-related.mjs +227 -0
- package/bin/tl-routes.mjs +627 -0
- package/bin/tl-search.mjs +123 -0
- package/bin/tl-structure.mjs +161 -0
- package/bin/tl-symbols.mjs +430 -0
- package/bin/tl-todo.mjs +341 -0
- package/bin/tl-types.mjs +441 -0
- package/bin/tl-unused.mjs +494 -0
- package/package.json +55 -0
- package/src/config.mjs +271 -0
- package/src/output.mjs +251 -0
- package/src/project.mjs +277 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-routes - Extract routes from web frameworks
|
|
5
|
+
*
|
|
6
|
+
* Scans source files for route definitions in common web frameworks
|
|
7
|
+
* (Next.js, React Router, Vue Router, Express, etc.) and shows the
|
|
8
|
+
* route structure.
|
|
9
|
+
*
|
|
10
|
+
* Usage: tl-routes [dir] [--tree]
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Prompt info for tl-prompt
|
|
14
|
+
if (process.argv.includes('--prompt')) {
|
|
15
|
+
console.log(JSON.stringify({
|
|
16
|
+
name: 'tl-routes',
|
|
17
|
+
desc: 'Extract routes from web frameworks',
|
|
18
|
+
when: 'before-read',
|
|
19
|
+
example: 'tl-routes src/'
|
|
20
|
+
}));
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
25
|
+
import { join, relative, extname, dirname, basename } from 'path';
|
|
26
|
+
import {
|
|
27
|
+
createOutput,
|
|
28
|
+
parseCommonArgs,
|
|
29
|
+
COMMON_OPTIONS_HELP
|
|
30
|
+
} from '../src/output.mjs';
|
|
31
|
+
import { findProjectRoot, shouldSkip } from '../src/project.mjs';
|
|
32
|
+
|
|
33
|
+
const HELP = `
|
|
34
|
+
tl-routes - Extract routes from web frameworks
|
|
35
|
+
|
|
36
|
+
Usage: tl-routes [dir] [options]
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--tree Show routes as tree structure
|
|
40
|
+
--with-components Show component/handler for each route
|
|
41
|
+
--framework <name> Force specific framework detection
|
|
42
|
+
${COMMON_OPTIONS_HELP}
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
tl-routes # Auto-detect framework
|
|
46
|
+
tl-routes src/ # Scan specific directory
|
|
47
|
+
tl-routes --tree # Tree view
|
|
48
|
+
tl-routes app/ --with-components
|
|
49
|
+
|
|
50
|
+
Detects:
|
|
51
|
+
- Next.js App Router (app/ directory structure)
|
|
52
|
+
- Next.js Pages Router (pages/ directory)
|
|
53
|
+
- React Router (createBrowserRouter, <Route>)
|
|
54
|
+
- Vue Router (createRouter, routes array)
|
|
55
|
+
- Express/Fastify routes
|
|
56
|
+
- SvelteKit (+page.svelte)
|
|
57
|
+
- Remix (routes/ directory)
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
// ─────────────────────────────────────────────────────────────
|
|
61
|
+
// Framework Detection
|
|
62
|
+
// ─────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function detectFramework(projectRoot) {
|
|
65
|
+
// Check package.json for framework hints
|
|
66
|
+
const pkgPath = join(projectRoot, 'package.json');
|
|
67
|
+
if (existsSync(pkgPath)) {
|
|
68
|
+
try {
|
|
69
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
70
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
71
|
+
|
|
72
|
+
if (deps['next']) return 'nextjs';
|
|
73
|
+
if (deps['react-router'] || deps['react-router-dom']) return 'react-router';
|
|
74
|
+
if (deps['vue-router']) return 'vue-router';
|
|
75
|
+
if (deps['@sveltejs/kit']) return 'sveltekit';
|
|
76
|
+
if (deps['@remix-run/react']) return 'remix';
|
|
77
|
+
if (deps['express']) return 'express';
|
|
78
|
+
if (deps['fastify']) return 'fastify';
|
|
79
|
+
if (deps['hono']) return 'hono';
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check for directory structures
|
|
84
|
+
if (existsSync(join(projectRoot, 'app')) && existsSync(join(projectRoot, 'app/page.tsx'))) {
|
|
85
|
+
return 'nextjs-app';
|
|
86
|
+
}
|
|
87
|
+
if (existsSync(join(projectRoot, 'src/app')) && existsSync(join(projectRoot, 'src/app/page.tsx'))) {
|
|
88
|
+
return 'nextjs-app';
|
|
89
|
+
}
|
|
90
|
+
if (existsSync(join(projectRoot, 'pages'))) {
|
|
91
|
+
return 'nextjs-pages';
|
|
92
|
+
}
|
|
93
|
+
if (existsSync(join(projectRoot, 'src/routes'))) {
|
|
94
|
+
return 'sveltekit';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return 'unknown';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─────────────────────────────────────────────────────────────
|
|
101
|
+
// Next.js App Router
|
|
102
|
+
// ─────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function extractNextAppRoutes(appDir, projectRoot) {
|
|
105
|
+
const routes = [];
|
|
106
|
+
|
|
107
|
+
function scanDir(dir, routePath = '') {
|
|
108
|
+
if (!existsSync(dir)) return;
|
|
109
|
+
|
|
110
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
111
|
+
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
|
|
114
|
+
|
|
115
|
+
const fullPath = join(dir, entry.name);
|
|
116
|
+
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
let segment = entry.name;
|
|
119
|
+
|
|
120
|
+
// Handle special Next.js conventions
|
|
121
|
+
if (segment.startsWith('(') && segment.endsWith(')')) {
|
|
122
|
+
// Route group - doesn't affect URL
|
|
123
|
+
scanDir(fullPath, routePath);
|
|
124
|
+
} else if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
125
|
+
// Dynamic segment
|
|
126
|
+
if (segment.startsWith('[...')) {
|
|
127
|
+
segment = '*'; // Catch-all
|
|
128
|
+
} else {
|
|
129
|
+
segment = `:${segment.slice(1, -1)}`;
|
|
130
|
+
}
|
|
131
|
+
scanDir(fullPath, `${routePath}/${segment}`);
|
|
132
|
+
} else if (segment.startsWith('@')) {
|
|
133
|
+
// Parallel route slot - skip
|
|
134
|
+
continue;
|
|
135
|
+
} else {
|
|
136
|
+
scanDir(fullPath, `${routePath}/${segment}`);
|
|
137
|
+
}
|
|
138
|
+
} else if (entry.isFile()) {
|
|
139
|
+
const name = entry.name.toLowerCase();
|
|
140
|
+
|
|
141
|
+
if (name === 'page.tsx' || name === 'page.jsx' || name === 'page.js' || name === 'page.ts') {
|
|
142
|
+
routes.push({
|
|
143
|
+
path: routePath || '/',
|
|
144
|
+
file: relative(projectRoot, fullPath),
|
|
145
|
+
type: 'page'
|
|
146
|
+
});
|
|
147
|
+
} else if (name === 'route.tsx' || name === 'route.ts' || name === 'route.js') {
|
|
148
|
+
routes.push({
|
|
149
|
+
path: routePath || '/',
|
|
150
|
+
file: relative(projectRoot, fullPath),
|
|
151
|
+
type: 'api'
|
|
152
|
+
});
|
|
153
|
+
} else if (name === 'layout.tsx' || name === 'layout.jsx' || name === 'layout.js') {
|
|
154
|
+
routes.push({
|
|
155
|
+
path: routePath || '/',
|
|
156
|
+
file: relative(projectRoot, fullPath),
|
|
157
|
+
type: 'layout'
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
scanDir(appDir);
|
|
165
|
+
return routes;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─────────────────────────────────────────────────────────────
|
|
169
|
+
// Next.js Pages Router
|
|
170
|
+
// ─────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
function extractNextPagesRoutes(pagesDir, projectRoot) {
|
|
173
|
+
const routes = [];
|
|
174
|
+
|
|
175
|
+
function scanDir(dir, routePath = '') {
|
|
176
|
+
if (!existsSync(dir)) return;
|
|
177
|
+
|
|
178
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
179
|
+
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
|
|
182
|
+
|
|
183
|
+
const fullPath = join(dir, entry.name);
|
|
184
|
+
|
|
185
|
+
if (entry.isDirectory()) {
|
|
186
|
+
let segment = entry.name;
|
|
187
|
+
|
|
188
|
+
if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
189
|
+
if (segment.startsWith('[...')) {
|
|
190
|
+
segment = '*';
|
|
191
|
+
} else {
|
|
192
|
+
segment = `:${segment.slice(1, -1)}`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
scanDir(fullPath, `${routePath}/${segment}`);
|
|
197
|
+
} else if (entry.isFile()) {
|
|
198
|
+
const ext = extname(entry.name);
|
|
199
|
+
if (!['.tsx', '.jsx', '.js', '.ts'].includes(ext)) continue;
|
|
200
|
+
|
|
201
|
+
const name = basename(entry.name, ext);
|
|
202
|
+
|
|
203
|
+
if (name === 'index') {
|
|
204
|
+
routes.push({
|
|
205
|
+
path: routePath || '/',
|
|
206
|
+
file: relative(projectRoot, fullPath),
|
|
207
|
+
type: routePath.startsWith('/api') ? 'api' : 'page'
|
|
208
|
+
});
|
|
209
|
+
} else if (name.startsWith('[') && name.endsWith(']')) {
|
|
210
|
+
let segment = name;
|
|
211
|
+
if (segment.startsWith('[...')) {
|
|
212
|
+
segment = '*';
|
|
213
|
+
} else {
|
|
214
|
+
segment = `:${segment.slice(1, -1)}`;
|
|
215
|
+
}
|
|
216
|
+
routes.push({
|
|
217
|
+
path: `${routePath}/${segment}`,
|
|
218
|
+
file: relative(projectRoot, fullPath),
|
|
219
|
+
type: routePath.startsWith('/api') ? 'api' : 'page'
|
|
220
|
+
});
|
|
221
|
+
} else {
|
|
222
|
+
routes.push({
|
|
223
|
+
path: `${routePath}/${name}`,
|
|
224
|
+
file: relative(projectRoot, fullPath),
|
|
225
|
+
type: routePath.startsWith('/api') ? 'api' : 'page'
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
scanDir(pagesDir);
|
|
233
|
+
return routes;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─────────────────────────────────────────────────────────────
|
|
237
|
+
// React Router
|
|
238
|
+
// ─────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
function extractReactRouterRoutes(content, filePath, projectRoot) {
|
|
241
|
+
const routes = [];
|
|
242
|
+
const relPath = relative(projectRoot, filePath);
|
|
243
|
+
|
|
244
|
+
// Match <Route path="..." element={...} /> or path: "..."
|
|
245
|
+
const routePatterns = [
|
|
246
|
+
/<Route\s+[^>]*path\s*=\s*["']([^"']+)["'][^>]*>/g,
|
|
247
|
+
/path\s*:\s*["']([^"']+)["']/g,
|
|
248
|
+
/createBrowserRouter\s*\(\s*\[[\s\S]*?path\s*:\s*["']([^"']+)["']/g
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
for (const pattern of routePatterns) {
|
|
252
|
+
let match;
|
|
253
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
254
|
+
const path = match[1];
|
|
255
|
+
if (!routes.some(r => r.path === path)) {
|
|
256
|
+
routes.push({
|
|
257
|
+
path,
|
|
258
|
+
file: relPath,
|
|
259
|
+
type: 'page'
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return routes;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─────────────────────────────────────────────────────────────
|
|
269
|
+
// Vue Router
|
|
270
|
+
// ─────────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
function extractVueRouterRoutes(content, filePath, projectRoot) {
|
|
273
|
+
const routes = [];
|
|
274
|
+
const relPath = relative(projectRoot, filePath);
|
|
275
|
+
|
|
276
|
+
// Match path: '/...' in route configs
|
|
277
|
+
const pathPattern = /path\s*:\s*['"]([^'"]+)['"]/g;
|
|
278
|
+
let match;
|
|
279
|
+
|
|
280
|
+
while ((match = pathPattern.exec(content)) !== null) {
|
|
281
|
+
const path = match[1];
|
|
282
|
+
if (!routes.some(r => r.path === path)) {
|
|
283
|
+
routes.push({
|
|
284
|
+
path,
|
|
285
|
+
file: relPath,
|
|
286
|
+
type: 'page'
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return routes;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ─────────────────────────────────────────────────────────────
|
|
295
|
+
// SvelteKit
|
|
296
|
+
// ─────────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
function extractSvelteKitRoutes(routesDir, projectRoot) {
|
|
299
|
+
const routes = [];
|
|
300
|
+
|
|
301
|
+
function scanDir(dir, routePath = '') {
|
|
302
|
+
if (!existsSync(dir)) return;
|
|
303
|
+
|
|
304
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
305
|
+
|
|
306
|
+
for (const entry of entries) {
|
|
307
|
+
if (entry.name.startsWith('.')) continue;
|
|
308
|
+
|
|
309
|
+
const fullPath = join(dir, entry.name);
|
|
310
|
+
|
|
311
|
+
if (entry.isDirectory()) {
|
|
312
|
+
let segment = entry.name;
|
|
313
|
+
|
|
314
|
+
if (segment.startsWith('(') && segment.endsWith(')')) {
|
|
315
|
+
scanDir(fullPath, routePath);
|
|
316
|
+
} else if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
317
|
+
segment = `:${segment.slice(1, -1)}`;
|
|
318
|
+
scanDir(fullPath, `${routePath}/${segment}`);
|
|
319
|
+
} else {
|
|
320
|
+
scanDir(fullPath, `${routePath}/${segment}`);
|
|
321
|
+
}
|
|
322
|
+
} else if (entry.isFile()) {
|
|
323
|
+
if (entry.name === '+page.svelte') {
|
|
324
|
+
routes.push({
|
|
325
|
+
path: routePath || '/',
|
|
326
|
+
file: relative(projectRoot, fullPath),
|
|
327
|
+
type: 'page'
|
|
328
|
+
});
|
|
329
|
+
} else if (entry.name === '+server.js' || entry.name === '+server.ts') {
|
|
330
|
+
routes.push({
|
|
331
|
+
path: routePath || '/',
|
|
332
|
+
file: relative(projectRoot, fullPath),
|
|
333
|
+
type: 'api'
|
|
334
|
+
});
|
|
335
|
+
} else if (entry.name === '+layout.svelte') {
|
|
336
|
+
routes.push({
|
|
337
|
+
path: routePath || '/',
|
|
338
|
+
file: relative(projectRoot, fullPath),
|
|
339
|
+
type: 'layout'
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
scanDir(routesDir);
|
|
347
|
+
return routes;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ─────────────────────────────────────────────────────────────
|
|
351
|
+
// File Discovery
|
|
352
|
+
// ─────────────────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
const CODE_EXTENSIONS = new Set(['.js', '.mjs', '.jsx', '.ts', '.tsx']);
|
|
355
|
+
|
|
356
|
+
function findCodeFiles(dir, files = []) {
|
|
357
|
+
if (!existsSync(dir)) return files;
|
|
358
|
+
|
|
359
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
360
|
+
|
|
361
|
+
for (const entry of entries) {
|
|
362
|
+
const fullPath = join(dir, entry.name);
|
|
363
|
+
|
|
364
|
+
if (entry.isDirectory()) {
|
|
365
|
+
if (!shouldSkip(entry.name, true)) {
|
|
366
|
+
findCodeFiles(fullPath, files);
|
|
367
|
+
}
|
|
368
|
+
} else if (entry.isFile()) {
|
|
369
|
+
const ext = extname(entry.name).toLowerCase();
|
|
370
|
+
if (CODE_EXTENSIONS.has(ext) && !shouldSkip(entry.name, false)) {
|
|
371
|
+
files.push(fullPath);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return files;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ─────────────────────────────────────────────────────────────
|
|
380
|
+
// Output Formatting
|
|
381
|
+
// ─────────────────────────────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
function buildRouteTree(routes) {
|
|
384
|
+
const tree = { children: {}, routes: [] };
|
|
385
|
+
|
|
386
|
+
for (const route of routes) {
|
|
387
|
+
const parts = route.path.split('/').filter(Boolean);
|
|
388
|
+
let node = tree;
|
|
389
|
+
|
|
390
|
+
for (const part of parts) {
|
|
391
|
+
if (!node.children[part]) {
|
|
392
|
+
node.children[part] = { children: {}, routes: [] };
|
|
393
|
+
}
|
|
394
|
+
node = node.children[part];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
node.routes.push(route);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return tree;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function printTree(node, out, prefix = '', isLast = true, path = '') {
|
|
404
|
+
const childKeys = Object.keys(node.children);
|
|
405
|
+
|
|
406
|
+
// Print routes at this level
|
|
407
|
+
for (const route of node.routes) {
|
|
408
|
+
const typeIcon = route.type === 'api' ? '⚡' : route.type === 'layout' ? '📐' : '📄';
|
|
409
|
+
out.add(`${prefix}${typeIcon} ${route.path || '/'}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Print children
|
|
413
|
+
childKeys.forEach((key, index) => {
|
|
414
|
+
const child = node.children[key];
|
|
415
|
+
const isLastChild = index === childKeys.length - 1;
|
|
416
|
+
const newPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
417
|
+
const branch = isLastChild ? '└─' : '├─';
|
|
418
|
+
|
|
419
|
+
out.add(`${prefix}${branch} /${key}`);
|
|
420
|
+
printTree(child, out, newPrefix, isLastChild, `${path}/${key}`);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ─────────────────────────────────────────────────────────────
|
|
425
|
+
// Main
|
|
426
|
+
// ─────────────────────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
const args = process.argv.slice(2);
|
|
429
|
+
const options = parseCommonArgs(args);
|
|
430
|
+
|
|
431
|
+
// Parse custom options
|
|
432
|
+
let treeView = false;
|
|
433
|
+
let withComponents = false;
|
|
434
|
+
let forcedFramework = null;
|
|
435
|
+
|
|
436
|
+
const remaining = [];
|
|
437
|
+
for (let i = 0; i < options.remaining.length; i++) {
|
|
438
|
+
const arg = options.remaining[i];
|
|
439
|
+
|
|
440
|
+
if (arg === '--tree') {
|
|
441
|
+
treeView = true;
|
|
442
|
+
} else if (arg === '--with-components') {
|
|
443
|
+
withComponents = true;
|
|
444
|
+
} else if (arg === '--framework') {
|
|
445
|
+
forcedFramework = options.remaining[++i];
|
|
446
|
+
} else if (!arg.startsWith('-')) {
|
|
447
|
+
remaining.push(arg);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const targetDir = remaining[0] || '.';
|
|
452
|
+
|
|
453
|
+
if (options.help) {
|
|
454
|
+
console.log(HELP);
|
|
455
|
+
process.exit(0);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (!existsSync(targetDir)) {
|
|
459
|
+
console.error(`Directory not found: ${targetDir}`);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Resolve target directory to absolute path
|
|
464
|
+
const targetAbsolute = targetDir.startsWith('/') ? targetDir : join(process.cwd(), targetDir);
|
|
465
|
+
|
|
466
|
+
// Use target directory as project root if it has package.json, otherwise find from cwd
|
|
467
|
+
const projectRoot = existsSync(join(targetAbsolute, 'package.json'))
|
|
468
|
+
? targetAbsolute
|
|
469
|
+
: findProjectRoot(targetAbsolute);
|
|
470
|
+
const out = createOutput(options);
|
|
471
|
+
|
|
472
|
+
// Detect framework from target directory
|
|
473
|
+
const framework = forcedFramework || detectFramework(targetAbsolute);
|
|
474
|
+
|
|
475
|
+
let routes = [];
|
|
476
|
+
|
|
477
|
+
// Extract routes based on framework
|
|
478
|
+
const searchRoot = targetAbsolute;
|
|
479
|
+
|
|
480
|
+
if (framework === 'nextjs' || framework === 'nextjs-app') {
|
|
481
|
+
// Try app directory first
|
|
482
|
+
const appDirs = [
|
|
483
|
+
join(searchRoot, 'app'),
|
|
484
|
+
join(searchRoot, 'src/app')
|
|
485
|
+
];
|
|
486
|
+
|
|
487
|
+
for (const appDir of appDirs) {
|
|
488
|
+
if (existsSync(appDir)) {
|
|
489
|
+
routes = extractNextAppRoutes(appDir, searchRoot);
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Also check pages directory
|
|
495
|
+
const pagesDirs = [
|
|
496
|
+
join(searchRoot, 'pages'),
|
|
497
|
+
join(searchRoot, 'src/pages')
|
|
498
|
+
];
|
|
499
|
+
|
|
500
|
+
for (const pagesDir of pagesDirs) {
|
|
501
|
+
if (existsSync(pagesDir)) {
|
|
502
|
+
routes.push(...extractNextPagesRoutes(pagesDir, searchRoot));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
} else if (framework === 'nextjs-pages') {
|
|
506
|
+
const pagesDirs = [
|
|
507
|
+
join(searchRoot, 'pages'),
|
|
508
|
+
join(searchRoot, 'src/pages')
|
|
509
|
+
];
|
|
510
|
+
|
|
511
|
+
for (const pagesDir of pagesDirs) {
|
|
512
|
+
if (existsSync(pagesDir)) {
|
|
513
|
+
routes = extractNextPagesRoutes(pagesDir, searchRoot);
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} else if (framework === 'sveltekit') {
|
|
518
|
+
const routesDirs = [
|
|
519
|
+
join(searchRoot, 'src/routes'),
|
|
520
|
+
join(searchRoot, 'routes')
|
|
521
|
+
];
|
|
522
|
+
|
|
523
|
+
for (const routesDir of routesDirs) {
|
|
524
|
+
if (existsSync(routesDir)) {
|
|
525
|
+
routes = extractSvelteKitRoutes(routesDir, searchRoot);
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} else if (framework === 'react-router' || framework === 'vue-router') {
|
|
530
|
+
// Scan code files for route definitions
|
|
531
|
+
const files = findCodeFiles(searchRoot);
|
|
532
|
+
|
|
533
|
+
for (const file of files) {
|
|
534
|
+
const content = readFileSync(file, 'utf-8');
|
|
535
|
+
|
|
536
|
+
if (framework === 'react-router') {
|
|
537
|
+
routes.push(...extractReactRouterRoutes(content, file, searchRoot));
|
|
538
|
+
} else {
|
|
539
|
+
routes.push(...extractVueRouterRoutes(content, file, searchRoot));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
// Generic scan - look for common route patterns
|
|
544
|
+
const files = findCodeFiles(searchRoot);
|
|
545
|
+
|
|
546
|
+
for (const file of files) {
|
|
547
|
+
const content = readFileSync(file, 'utf-8');
|
|
548
|
+
routes.push(...extractReactRouterRoutes(content, file, searchRoot));
|
|
549
|
+
routes.push(...extractVueRouterRoutes(content, file, searchRoot));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Deduplicate routes
|
|
554
|
+
const seen = new Set();
|
|
555
|
+
routes = routes.filter(r => {
|
|
556
|
+
const key = `${r.path}:${r.type}`;
|
|
557
|
+
if (seen.has(key)) return false;
|
|
558
|
+
seen.add(key);
|
|
559
|
+
return true;
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// Sort routes
|
|
563
|
+
routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
564
|
+
|
|
565
|
+
// Set JSON data
|
|
566
|
+
out.setData('framework', framework);
|
|
567
|
+
out.setData('routes', routes);
|
|
568
|
+
out.setData('totalRoutes', routes.length);
|
|
569
|
+
|
|
570
|
+
// Output
|
|
571
|
+
out.header(`🛤️ Routes (${framework})`);
|
|
572
|
+
out.blank();
|
|
573
|
+
|
|
574
|
+
if (routes.length === 0) {
|
|
575
|
+
out.add('No routes found');
|
|
576
|
+
} else if (treeView) {
|
|
577
|
+
const tree = buildRouteTree(routes);
|
|
578
|
+
printTree(tree, out);
|
|
579
|
+
} else {
|
|
580
|
+
// Group by type
|
|
581
|
+
const pages = routes.filter(r => r.type === 'page');
|
|
582
|
+
const apis = routes.filter(r => r.type === 'api');
|
|
583
|
+
const layouts = routes.filter(r => r.type === 'layout');
|
|
584
|
+
|
|
585
|
+
if (pages.length > 0) {
|
|
586
|
+
out.add('Pages:');
|
|
587
|
+
for (const route of pages) {
|
|
588
|
+
const component = withComponents ? ` → ${route.file}` : '';
|
|
589
|
+
out.add(` ${route.path}${component}`);
|
|
590
|
+
}
|
|
591
|
+
out.blank();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (apis.length > 0) {
|
|
595
|
+
out.add('API Routes:');
|
|
596
|
+
for (const route of apis) {
|
|
597
|
+
const component = withComponents ? ` → ${route.file}` : '';
|
|
598
|
+
out.add(` ${route.path}${component}`);
|
|
599
|
+
}
|
|
600
|
+
out.blank();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (layouts.length > 0) {
|
|
604
|
+
out.add('Layouts:');
|
|
605
|
+
for (const route of layouts) {
|
|
606
|
+
const component = withComponents ? ` → ${route.file}` : '';
|
|
607
|
+
out.add(` ${route.path}${component}`);
|
|
608
|
+
}
|
|
609
|
+
out.blank();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Summary
|
|
614
|
+
if (!options.quiet && routes.length > 0) {
|
|
615
|
+
const pages = routes.filter(r => r.type === 'page').length;
|
|
616
|
+
const apis = routes.filter(r => r.type === 'api').length;
|
|
617
|
+
const layouts = routes.filter(r => r.type === 'layout').length;
|
|
618
|
+
|
|
619
|
+
const parts = [];
|
|
620
|
+
if (pages > 0) parts.push(`${pages} pages`);
|
|
621
|
+
if (apis > 0) parts.push(`${apis} API`);
|
|
622
|
+
if (layouts > 0) parts.push(`${layouts} layouts`);
|
|
623
|
+
|
|
624
|
+
out.add(`Total: ${parts.join(', ')}`);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
out.print();
|