next-route-graph 1.0.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/LICENSE +21 -0
- package/README.md +367 -0
- package/dist/cli.js +58 -0
- package/dist/core/analyzer.js +85 -0
- package/dist/core/detector.js +28 -0
- package/dist/core/parser.js +78 -0
- package/dist/core/scanner.js +45 -0
- package/dist/render/flat.js +3 -0
- package/dist/render/json.js +16 -0
- package/dist/render/tree.js +45 -0
- package/dist/types/index.js +1 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Next Route Map Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom me the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# next-route-graph
|
|
2
|
+
|
|
3
|
+
> Visualize and inspect the structure of a Next.js App Router project directly from your terminal.
|
|
4
|
+
|
|
5
|
+
`next-route-graph` is a lightweight, fast, zero-config CLI tool for Next.js developers.
|
|
6
|
+
|
|
7
|
+
It scans your local App Router structure and provides useful information about routes, dynamic segments, rendering types, layouts, route handlers, and special Next.js files — without requiring any configuration or network requests.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- ⚡ **Zero-config** — Works out of the box with no configuration files.
|
|
12
|
+
- 🚫 **Zero-network** — Analyzes your project locally without sending files or data anywhere.
|
|
13
|
+
- 🎯 **App Router focused** — Automatically detects both `app/` and `src/app/`.
|
|
14
|
+
- 📁 **Dynamic route support** — Detects dynamic, catch-all, and optional catch-all segments.
|
|
15
|
+
- 🧩 **Advanced routing support** — Recognizes Route Groups, Parallel Routes, and Intercepting Routes.
|
|
16
|
+
- 🖥️ **Rendering detection** — Identifies Server and Client components.
|
|
17
|
+
- 📄 **Special files** — Detects Next.js App Router special files and route handlers.
|
|
18
|
+
- 🔍 **Search & filter** — Quickly find routes matching a specific query.
|
|
19
|
+
- 📊 **Multiple output formats** — Human-readable tree, flat route list, or machine-readable JSON.
|
|
20
|
+
- 🚀 **Fast & lightweight** — No Next.js runtime or project configuration required.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
### Run with npx
|
|
25
|
+
|
|
26
|
+
The easiest way to use `next-route-graph`:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx next-route-graph
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Install globally
|
|
33
|
+
|
|
34
|
+
You can also install it globally:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install -g next-route-graph
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then run:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
next-route-graph
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
Run the CLI from the root of your Next.js project:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx next-route-graph
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The CLI automatically detects your App Router directory:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
app/
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
or:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
src/app/
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
You can also provide a custom project path:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx next-route-graph ./my-next-app
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Example
|
|
73
|
+
|
|
74
|
+
For a project with the following structure:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
src/app/
|
|
78
|
+
├── [locale]/
|
|
79
|
+
│ ├── about/
|
|
80
|
+
│ │ └── page.tsx
|
|
81
|
+
│ ├── concepts/
|
|
82
|
+
│ │ ├── [category]/
|
|
83
|
+
│ │ │ └── [slug]/
|
|
84
|
+
│ │ │ └── page.tsx
|
|
85
|
+
│ │ └── page.tsx
|
|
86
|
+
│ └── page.tsx
|
|
87
|
+
├── api/
|
|
88
|
+
│ └── route.ts
|
|
89
|
+
└── layout.tsx
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`next-route-graph` produces a route overview such as:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
Project my-next-app
|
|
96
|
+
Router App Router
|
|
97
|
+
Root src/app
|
|
98
|
+
|
|
99
|
+
Routes
|
|
100
|
+
────────────────────────────────────────
|
|
101
|
+
├── /[locale] page.tsx [dynamic]
|
|
102
|
+
├── /[locale]/about page.tsx [dynamic]
|
|
103
|
+
├── /[locale]/concepts page.tsx [dynamic]
|
|
104
|
+
├── /[locale]/concepts/[category] page.tsx [dynamic]
|
|
105
|
+
├── /[locale]/concepts/[category]/[slug] page.tsx [dynamic]
|
|
106
|
+
└── /api route.ts
|
|
107
|
+
|
|
108
|
+
Summary
|
|
109
|
+
────────────────────────────────────────
|
|
110
|
+
Routes 6
|
|
111
|
+
Dynamic 5
|
|
112
|
+
Client 0
|
|
113
|
+
Server 6
|
|
114
|
+
Layouts 1
|
|
115
|
+
Loading 0
|
|
116
|
+
Errors 0
|
|
117
|
+
|
|
118
|
+
✓ Analysis completed in 18ms
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## CLI Options
|
|
122
|
+
|
|
123
|
+
### `--flat`
|
|
124
|
+
|
|
125
|
+
Output only route paths as a flat list.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx next-route-graph --flat
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
/[locale]
|
|
135
|
+
/[locale]/about
|
|
136
|
+
/[locale]/concepts
|
|
137
|
+
/[locale]/concepts/[category]
|
|
138
|
+
/api
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This format is useful when you only need the route paths without additional metadata.
|
|
142
|
+
|
|
143
|
+
### `--json`
|
|
144
|
+
|
|
145
|
+
Output the complete analysis as machine-readable JSON.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npx next-route-graph --json
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
This is useful for scripts, automation, CI workflows, and other development tools.
|
|
152
|
+
|
|
153
|
+
### `--search <query>`
|
|
154
|
+
|
|
155
|
+
Filter routes using a search query.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npx next-route-graph --search blog
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
For example:
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
/blog
|
|
165
|
+
/blog/[slug]
|
|
166
|
+
/blog/archive
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Search can be combined with other output modes:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npx next-route-graph --search blog --flat
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
or:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npx next-route-graph --search blog --json
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### `<projectPath>`
|
|
182
|
+
|
|
183
|
+
Analyze a specific Next.js project instead of the current directory.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
npx next-route-graph ./my-next-app
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Command Reference
|
|
190
|
+
|
|
191
|
+
| Command / Option | Description |
|
|
192
|
+
| -------------------------------- | ----------------------------------- |
|
|
193
|
+
| `next-route-graph` | Analyze the current Next.js project |
|
|
194
|
+
| `next-route-graph <projectPath>` | Analyze a specific project |
|
|
195
|
+
| `--flat` | Output only route paths |
|
|
196
|
+
| `--json` | Output machine-readable JSON |
|
|
197
|
+
| `--search <query>` | Filter routes by search string |
|
|
198
|
+
| `--help` | Show CLI help |
|
|
199
|
+
| `--version` | Show the installed version |
|
|
200
|
+
|
|
201
|
+
## Supported Next.js Routing Features
|
|
202
|
+
|
|
203
|
+
`next-route-graph` understands common Next.js App Router conventions, including:
|
|
204
|
+
|
|
205
|
+
### Static Routes
|
|
206
|
+
|
|
207
|
+
```text
|
|
208
|
+
/about
|
|
209
|
+
/dashboard
|
|
210
|
+
/settings/profile
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Dynamic Routes
|
|
214
|
+
|
|
215
|
+
```text
|
|
216
|
+
/[id]
|
|
217
|
+
/products/[productId]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Catch-all Routes
|
|
221
|
+
|
|
222
|
+
```text
|
|
223
|
+
/docs/[...slug]
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Optional Catch-all Routes
|
|
227
|
+
|
|
228
|
+
```text
|
|
229
|
+
/docs/[[...slug]]
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Route Groups
|
|
233
|
+
|
|
234
|
+
```text
|
|
235
|
+
/(marketing)/about
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Parallel Routes
|
|
239
|
+
|
|
240
|
+
```text
|
|
241
|
+
/dashboard/@analytics
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Intercepting Routes
|
|
245
|
+
|
|
246
|
+
```text
|
|
247
|
+
(.)photo
|
|
248
|
+
(..)photo
|
|
249
|
+
(...)photo
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Route Handlers
|
|
253
|
+
|
|
254
|
+
```text
|
|
255
|
+
/api/users/route.ts
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Special App Router Files
|
|
259
|
+
|
|
260
|
+
The analyzer detects relevant Next.js App Router files such as:
|
|
261
|
+
|
|
262
|
+
```text
|
|
263
|
+
layout.tsx
|
|
264
|
+
page.tsx
|
|
265
|
+
loading.tsx
|
|
266
|
+
error.tsx
|
|
267
|
+
not-found.tsx
|
|
268
|
+
route.ts
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Output Formats
|
|
272
|
+
|
|
273
|
+
`next-route-graph` currently provides three output modes.
|
|
274
|
+
|
|
275
|
+
### Tree
|
|
276
|
+
|
|
277
|
+
The default output provides a human-readable overview of the project.
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
npx next-route-graph
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Flat
|
|
284
|
+
|
|
285
|
+
Outputs only route paths.
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
npx next-route-graph --flat
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### JSON
|
|
292
|
+
|
|
293
|
+
Outputs structured data for programmatic use.
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
npx next-route-graph --json
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Why next-route-graph?
|
|
300
|
+
|
|
301
|
+
Next.js App Router projects can become difficult to navigate as the number of routes, layouts, dynamic segments, and special files grows.
|
|
302
|
+
|
|
303
|
+
`next-route-graph` gives you a quick overview of the routing structure directly from your terminal.
|
|
304
|
+
|
|
305
|
+
No configuration.
|
|
306
|
+
|
|
307
|
+
No network requests.
|
|
308
|
+
|
|
309
|
+
No Next.js runtime.
|
|
310
|
+
|
|
311
|
+
Just run it and inspect your routes.
|
|
312
|
+
|
|
313
|
+
## Development
|
|
314
|
+
|
|
315
|
+
Clone the repository:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
git clone https://github.com/Nima-sltn/next-route-graph.git
|
|
319
|
+
cd next-route-graph
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Install dependencies:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
npm install
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Run tests:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
npm test
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Run type checking:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
npm run typecheck
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Build the project:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
npm run build
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Contributing
|
|
347
|
+
|
|
348
|
+
Contributions, bug reports, feature requests, and improvements are welcome.
|
|
349
|
+
|
|
350
|
+
If you find an issue or have an idea for improving `next-route-graph`, feel free to open an issue or submit a pull request.
|
|
351
|
+
|
|
352
|
+
Before submitting a pull request, please make sure that:
|
|
353
|
+
|
|
354
|
+
- Tests are passing.
|
|
355
|
+
- Type checking passes.
|
|
356
|
+
- The project builds successfully.
|
|
357
|
+
|
|
358
|
+
## License
|
|
359
|
+
|
|
360
|
+
MIT © 2026 Next Route Map Contributors
|
|
361
|
+
|
|
362
|
+
See the [LICENSE](./LICENSE) file for the full license text.
|
|
363
|
+
|
|
364
|
+
## Links
|
|
365
|
+
|
|
366
|
+
- **npm:** https://www.npmjs.com/package/next-route-graph
|
|
367
|
+
- **GitHub:** https://github.com/Nima-sltn/next-route-graph
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { analyzeRoutes } from "./core/analyzer.js";
|
|
5
|
+
import { detectAppRouter } from "./core/detector.js";
|
|
6
|
+
import { scanAppDir } from "./core/scanner.js";
|
|
7
|
+
import { renderFlat } from "./render/flat.js";
|
|
8
|
+
import { renderJson } from "./render/json.js";
|
|
9
|
+
import { renderTree } from "./render/tree.js";
|
|
10
|
+
const program = new Command();
|
|
11
|
+
program
|
|
12
|
+
.name("next-route-graph")
|
|
13
|
+
.description("Inspect and visualize Next.js App Router project routes")
|
|
14
|
+
.version("1.0.0")
|
|
15
|
+
.argument("[projectPath]", "Path to the Next.js project root", ".")
|
|
16
|
+
.option("--flat", "Output only flat route paths")
|
|
17
|
+
.option("--json", "Output machine-readable JSON")
|
|
18
|
+
.option("--search <query>", "Filter routes by search string")
|
|
19
|
+
.action(async (projectPath, options) => {
|
|
20
|
+
try {
|
|
21
|
+
const { appDir, displayRoot } = detectAppRouter(projectPath);
|
|
22
|
+
const scannedFiles = await scanAppDir(appDir);
|
|
23
|
+
let analysisResult = await analyzeRoutes(scannedFiles, projectPath, displayRoot);
|
|
24
|
+
if (options.search) {
|
|
25
|
+
const search = options.search;
|
|
26
|
+
analysisResult.routes = analysisResult.routes.filter((route) => route.path.includes(search));
|
|
27
|
+
analysisResult.summary = {
|
|
28
|
+
...analysisResult.summary,
|
|
29
|
+
routes: analysisResult.routes.length,
|
|
30
|
+
dynamic: analysisResult.routes.filter((route) => route.segmentType === "dynamic").length,
|
|
31
|
+
client: analysisResult.routes.filter((route) => route.rendering === "client").length,
|
|
32
|
+
server: analysisResult.routes.filter((route) => route.rendering === "server").length,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (options.json) {
|
|
36
|
+
console.log(renderJson(analysisResult));
|
|
37
|
+
}
|
|
38
|
+
else if (options.flat) {
|
|
39
|
+
const flatOutput = renderFlat(analysisResult.routes);
|
|
40
|
+
if (flatOutput) {
|
|
41
|
+
console.log(flatOutput);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.log(renderTree(analysisResult));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (options.json) {
|
|
50
|
+
console.error(JSON.stringify({ error: error.message }));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.error(pc.red(`✗ ${error.message}`));
|
|
54
|
+
}
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { detectRenderingType, getSegmentType, parseRoutePath, parseSpecialFileType, } from "./parser.js";
|
|
3
|
+
export async function analyzeRoutes(files, projectPath, displayRoot) {
|
|
4
|
+
const startTime = Date.now();
|
|
5
|
+
const dirMap = new Map();
|
|
6
|
+
for (const file of files) {
|
|
7
|
+
const dir = path.dirname(file.relativePath).replace(/\\/g, "/");
|
|
8
|
+
if (!dirMap.has(dir)) {
|
|
9
|
+
dirMap.set(dir, []);
|
|
10
|
+
}
|
|
11
|
+
dirMap.get(dir).push(file);
|
|
12
|
+
}
|
|
13
|
+
const routesMap = new Map();
|
|
14
|
+
let totalLayouts = 0;
|
|
15
|
+
let totalLoading = 0;
|
|
16
|
+
let totalErrors = 0;
|
|
17
|
+
for (const [dir, dirFiles] of dirMap.entries()) {
|
|
18
|
+
const routeFiles = [];
|
|
19
|
+
for (const file of dirFiles) {
|
|
20
|
+
const specialType = parseSpecialFileType(file.fileName);
|
|
21
|
+
if (specialType) {
|
|
22
|
+
const rendering = await detectRenderingType(file.fullPath);
|
|
23
|
+
routeFiles.push({
|
|
24
|
+
type: specialType,
|
|
25
|
+
fileName: file.fileName,
|
|
26
|
+
relativePath: file.relativePath,
|
|
27
|
+
rendering,
|
|
28
|
+
});
|
|
29
|
+
if (specialType === "layout")
|
|
30
|
+
totalLayouts++;
|
|
31
|
+
if (specialType === "loading")
|
|
32
|
+
totalLoading++;
|
|
33
|
+
if (specialType === "error")
|
|
34
|
+
totalErrors++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const pageFile = routeFiles.find((f) => f.type === "page");
|
|
38
|
+
const routeHandlerFile = routeFiles.find((f) => f.type === "route");
|
|
39
|
+
if (pageFile || routeHandlerFile) {
|
|
40
|
+
const primaryFile = (pageFile || routeHandlerFile);
|
|
41
|
+
const computedPath = parseRoutePath(primaryFile.relativePath);
|
|
42
|
+
routesMap.set(computedPath, {
|
|
43
|
+
path: computedPath,
|
|
44
|
+
type: pageFile ? "page" : "route",
|
|
45
|
+
file: primaryFile.relativePath,
|
|
46
|
+
segmentType: getSegmentType(computedPath),
|
|
47
|
+
rendering: primaryFile.rendering,
|
|
48
|
+
files: routeFiles,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const routes = Array.from(routesMap.values()).sort((a, b) => a.path.localeCompare(b.path));
|
|
53
|
+
let dynamicCount = 0;
|
|
54
|
+
let clientCount = 0;
|
|
55
|
+
let serverCount = 0;
|
|
56
|
+
for (const route of routes) {
|
|
57
|
+
if (route.segmentType !== "static") {
|
|
58
|
+
dynamicCount++;
|
|
59
|
+
}
|
|
60
|
+
if (route.rendering === "client") {
|
|
61
|
+
clientCount++;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
serverCount++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const projectName = path.basename(path.resolve(projectPath));
|
|
68
|
+
const summary = {
|
|
69
|
+
routes: routes.length,
|
|
70
|
+
dynamic: dynamicCount,
|
|
71
|
+
client: clientCount,
|
|
72
|
+
server: serverCount,
|
|
73
|
+
layouts: totalLayouts,
|
|
74
|
+
loading: totalLoading,
|
|
75
|
+
errors: totalErrors,
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
project: projectName,
|
|
79
|
+
router: "App Router",
|
|
80
|
+
root: displayRoot,
|
|
81
|
+
routes,
|
|
82
|
+
summary,
|
|
83
|
+
executionTimeMs: Date.now() - startTime,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function detectAppRouter(projectDir) {
|
|
4
|
+
const absoluteDir = path.resolve(projectDir);
|
|
5
|
+
if (!fs.existsSync(absoluteDir)) {
|
|
6
|
+
throw new Error("The specified project path does not exist.");
|
|
7
|
+
}
|
|
8
|
+
const srcAppPath = path.join(absoluteDir, "src", "app");
|
|
9
|
+
const appPath = path.join(absoluteDir, "app");
|
|
10
|
+
const hasSrcApp = fs.existsSync(srcAppPath) && fs.statSync(srcAppPath).isDirectory();
|
|
11
|
+
const hasApp = fs.existsSync(appPath) && fs.statSync(appPath).isDirectory();
|
|
12
|
+
if (hasSrcApp && hasApp) {
|
|
13
|
+
throw new Error(`Could not determine the App Router root.\n\nFound both:\n ./app\n ./src/app\n\nPlease keep only one App Router root for analysis.`);
|
|
14
|
+
}
|
|
15
|
+
if (hasSrcApp) {
|
|
16
|
+
return {
|
|
17
|
+
appDir: srcAppPath,
|
|
18
|
+
displayRoot: "src/app",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (hasApp) {
|
|
22
|
+
return {
|
|
23
|
+
appDir: appPath,
|
|
24
|
+
displayRoot: "app",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`No Next.js App Router directory found.\n\nExpected:\n ./app\n ./src/app`);
|
|
28
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const SPECIAL_FILES = new Set([
|
|
4
|
+
"page",
|
|
5
|
+
"layout",
|
|
6
|
+
"loading",
|
|
7
|
+
"error",
|
|
8
|
+
"not-found",
|
|
9
|
+
"template",
|
|
10
|
+
"default",
|
|
11
|
+
"route",
|
|
12
|
+
]);
|
|
13
|
+
export function parseSpecialFileType(fileName) {
|
|
14
|
+
const baseName = path.parse(fileName).name;
|
|
15
|
+
if (SPECIAL_FILES.has(baseName)) {
|
|
16
|
+
return baseName;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export async function detectRenderingType(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
23
|
+
const firstLines = content.slice(0, 500);
|
|
24
|
+
const clientDirectiveRegex = /^\s*(?:\/\/.*?\n|\/\*[\s\S]*?\*\/)*\s*["']use client["']/m;
|
|
25
|
+
if (clientDirectiveRegex.test(firstLines)) {
|
|
26
|
+
return "client";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Default to server if unreadable
|
|
31
|
+
}
|
|
32
|
+
return "server";
|
|
33
|
+
}
|
|
34
|
+
export function parseRoutePath(relativePath) {
|
|
35
|
+
const parts = relativePath.split("/");
|
|
36
|
+
// Remove the file name at the end
|
|
37
|
+
parts.pop();
|
|
38
|
+
const urlSegments = [];
|
|
39
|
+
for (const part of parts) {
|
|
40
|
+
if (!part)
|
|
41
|
+
continue;
|
|
42
|
+
// Route groups are ignored in URL matching
|
|
43
|
+
if (part.startsWith("(") && part.endsWith(")")) {
|
|
44
|
+
const inner = part.slice(1, -1);
|
|
45
|
+
// Exclude intercepting routes which also start with '('
|
|
46
|
+
if (!inner.startsWith(".") && inner !== "...") {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Parallel routes are ignored in URL pathing
|
|
51
|
+
if (part.startsWith("@")) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Intercepting routes clean-up for display path
|
|
55
|
+
let cleanedPart = part;
|
|
56
|
+
if (cleanedPart.startsWith("(.)") ||
|
|
57
|
+
cleanedPart.startsWith("(..)") ||
|
|
58
|
+
cleanedPart.startsWith("(...)") ||
|
|
59
|
+
cleanedPart.startsWith("(..)(..)")) {
|
|
60
|
+
cleanedPart = cleanedPart.replace(/^\(\.\.\.\)|\(\.\.\)|\(\.\)/, "");
|
|
61
|
+
}
|
|
62
|
+
urlSegments.push(cleanedPart);
|
|
63
|
+
}
|
|
64
|
+
const routePath = "/" + urlSegments.join("/");
|
|
65
|
+
return routePath === "/" ? "/" : routePath.replace(/\/+/g, "/");
|
|
66
|
+
}
|
|
67
|
+
export function getSegmentType(routePath) {
|
|
68
|
+
if (routePath.includes("[[...")) {
|
|
69
|
+
return "optional-catch-all";
|
|
70
|
+
}
|
|
71
|
+
if (routePath.includes("[...")) {
|
|
72
|
+
return "catch-all";
|
|
73
|
+
}
|
|
74
|
+
if (routePath.includes("[")) {
|
|
75
|
+
return "dynamic";
|
|
76
|
+
}
|
|
77
|
+
return "static";
|
|
78
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const ALLOWED_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
4
|
+
const IGNORE_DIRS = new Set([
|
|
5
|
+
"node_modules",
|
|
6
|
+
".git",
|
|
7
|
+
".next",
|
|
8
|
+
"dist",
|
|
9
|
+
"build",
|
|
10
|
+
"coverage",
|
|
11
|
+
]);
|
|
12
|
+
export async function scanAppDir(rootDir, currentDir = rootDir) {
|
|
13
|
+
let files = [];
|
|
14
|
+
let entries;
|
|
15
|
+
try {
|
|
16
|
+
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error("Unable to read the App Router directory.");
|
|
20
|
+
}
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
if (!IGNORE_DIRS.has(entry.name)) {
|
|
25
|
+
const subFiles = await scanAppDir(rootDir, fullPath);
|
|
26
|
+
files = files.concat(subFiles);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (entry.isFile()) {
|
|
30
|
+
const ext = path.extname(entry.name);
|
|
31
|
+
if (ALLOWED_EXTENSIONS.has(ext)) {
|
|
32
|
+
const relativePath = path
|
|
33
|
+
.relative(rootDir, fullPath)
|
|
34
|
+
.replace(/\\/g, "/");
|
|
35
|
+
files.push({
|
|
36
|
+
relativePath,
|
|
37
|
+
fullPath,
|
|
38
|
+
fileName: entry.name,
|
|
39
|
+
extension: ext,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return files;
|
|
45
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function renderJson(result) {
|
|
2
|
+
const output = {
|
|
3
|
+
project: result.project,
|
|
4
|
+
router: "app",
|
|
5
|
+
root: result.root,
|
|
6
|
+
routes: result.routes.map((r) => ({
|
|
7
|
+
path: r.path,
|
|
8
|
+
type: r.type,
|
|
9
|
+
file: r.file,
|
|
10
|
+
segmentType: r.segmentType,
|
|
11
|
+
rendering: r.rendering,
|
|
12
|
+
})),
|
|
13
|
+
summary: result.summary,
|
|
14
|
+
};
|
|
15
|
+
return JSON.stringify(output, null, 2);
|
|
16
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
export function renderTree(result) {
|
|
3
|
+
const lines = [];
|
|
4
|
+
lines.push("");
|
|
5
|
+
lines.push(pc.bold("next-route-graph"));
|
|
6
|
+
lines.push("");
|
|
7
|
+
lines.push(`${pc.dim("Project ")} ${result.project}`);
|
|
8
|
+
lines.push(`${pc.dim("Router ")} ${result.router}`);
|
|
9
|
+
lines.push(`${pc.dim("Root ")} ${result.root}`);
|
|
10
|
+
lines.push("");
|
|
11
|
+
lines.push(pc.bold("Routes"));
|
|
12
|
+
lines.push(pc.dim("────────────────────────────────────────"));
|
|
13
|
+
const routes = result.routes;
|
|
14
|
+
routes.forEach((route, index) => {
|
|
15
|
+
const isLast = index === routes.length - 1;
|
|
16
|
+
const prefix = isLast ? "└── " : "├── ";
|
|
17
|
+
const pathStr = route.path.padEnd(28, " ");
|
|
18
|
+
const fileStr = pc.dim(route.file.split("/").pop() || "");
|
|
19
|
+
let badge = "";
|
|
20
|
+
if (route.rendering === "client") {
|
|
21
|
+
badge = ` ${pc.cyan("[client]")}`;
|
|
22
|
+
}
|
|
23
|
+
else if (route.segmentType !== "static") {
|
|
24
|
+
badge = ` ${pc.yellow(`[${route.segmentType}]`)}`;
|
|
25
|
+
}
|
|
26
|
+
lines.push(`${pc.dim(prefix)}${pc.bold(pathStr)} ${fileStr}${badge}`);
|
|
27
|
+
});
|
|
28
|
+
if (routes.length === 0) {
|
|
29
|
+
lines.push(pc.dim(" (No routes found)"));
|
|
30
|
+
}
|
|
31
|
+
lines.push("");
|
|
32
|
+
lines.push(pc.bold("Summary"));
|
|
33
|
+
lines.push(pc.dim("────────────────────────────────────────"));
|
|
34
|
+
lines.push(`${pc.dim("Routes ")} ${result.summary.routes}`);
|
|
35
|
+
lines.push(`${pc.dim("Dynamic ")} ${result.summary.dynamic}`);
|
|
36
|
+
lines.push(`${pc.dim("Client ")} ${result.summary.client}`);
|
|
37
|
+
lines.push(`${pc.dim("Server ")} ${result.summary.server}`);
|
|
38
|
+
lines.push(`${pc.dim("Layouts ")} ${result.summary.layouts}`);
|
|
39
|
+
lines.push(`${pc.dim("Loading ")} ${result.summary.loading}`);
|
|
40
|
+
lines.push(`${pc.dim("Errors ")} ${result.summary.errors}`);
|
|
41
|
+
lines.push("");
|
|
42
|
+
lines.push(pc.green(`✓ Analysis completed in ${result.executionTimeMs}ms`));
|
|
43
|
+
lines.push("");
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "next-route-graph",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Visualize and inspect the structure of a Next.js App Router project from the terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"next-route-graph": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "tsx src/cli.ts",
|
|
17
|
+
"build": "tsc -p tsconfig.build.json",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"test": "tsx --test tests/**/*.test.ts",
|
|
20
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"nextjs",
|
|
24
|
+
"app-router",
|
|
25
|
+
"routes",
|
|
26
|
+
"cli",
|
|
27
|
+
"visualization",
|
|
28
|
+
"route-map",
|
|
29
|
+
"typescript"
|
|
30
|
+
],
|
|
31
|
+
"author": "Next Route Map Contributors",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^12.1.0",
|
|
35
|
+
"picocolors": "^1.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.19.43",
|
|
39
|
+
"tsx": "^4.7.0",
|
|
40
|
+
"typescript": "^5.3.3"
|
|
41
|
+
}
|
|
42
|
+
}
|