apiforge-analyzer 1.1.0 → 1.3.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 +166 -197
- package/dist/cjs/arch-detect.js +133 -0
- package/dist/cjs/cli.js +866 -244
- package/dist/cjs/detect.js +68 -48
- package/dist/cjs/export.js +2 -0
- package/dist/cjs/frameworks/express.js +80 -74
- package/dist/cjs/frameworks/nestjs.js +38 -0
- package/dist/cjs/index.js +158 -60
- package/dist/cjs/modules/express.js +65 -0
- package/dist/cjs/modules/nestjs.js +50 -0
- package/dist/cjs/uploader.js +51 -11
- package/dist/esm/arch-detect.js +130 -0
- package/dist/esm/cli.js +865 -246
- package/dist/esm/detect.js +68 -48
- package/dist/esm/export.js +2 -0
- package/dist/esm/frameworks/express.js +80 -74
- package/dist/esm/frameworks/nestjs.js +35 -0
- package/dist/esm/index.js +158 -62
- package/dist/esm/modules/express.js +60 -0
- package/dist/esm/modules/nestjs.js +47 -0
- package/dist/esm/uploader.js +49 -11
- package/dist/types/arch-detect.d.ts +9 -0
- package/dist/types/detect.d.ts +3 -5
- package/dist/types/export.d.ts +2 -0
- package/dist/types/frameworks/express.d.ts +2 -2
- package/dist/types/frameworks/nestjs.d.ts +10 -0
- package/dist/types/index.d.ts +12 -5
- package/dist/types/modules/express.d.ts +22 -0
- package/dist/types/modules/nestjs.d.ts +6 -0
- package/dist/types/uploader.d.ts +17 -2
- package/package.json +3 -2
package/dist/esm/detect.js
CHANGED
|
@@ -1,65 +1,85 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
const cwd = () => process.cwd();
|
|
4
|
+
function fileExists(rel) {
|
|
5
|
+
return existsSync(join(cwd(), rel));
|
|
6
|
+
}
|
|
7
|
+
function tryResolve(mod) {
|
|
8
|
+
try {
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
10
|
+
require.resolve(mod, { paths: [cwd()] });
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
1
17
|
/**
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Detect which framework is being used
|
|
18
|
+
* Detect the web framework from the live app instance + filesystem hints.
|
|
19
|
+
* Confidence is used to pick the best match when multiple signals fire.
|
|
6
20
|
*/
|
|
7
21
|
export function detectFramework(app) {
|
|
8
|
-
//
|
|
22
|
+
// ── Instance-based detection (highest confidence) ─────────────────────────
|
|
9
23
|
if (app && typeof app === 'function') {
|
|
24
|
+
// Express: _router or lazyrouter
|
|
10
25
|
if (app._router || (app.lazyrouter && typeof app.lazyrouter === 'function')) {
|
|
11
26
|
return { name: 'express', confidence: 1.0 };
|
|
12
27
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const fs = require('fs');
|
|
17
|
-
const path = require('path');
|
|
18
|
-
// Check for Next.js Pages Router
|
|
19
|
-
if (fs.existsSync(path.join(process.cwd(), 'pages'))) {
|
|
20
|
-
const hasApiFolder = fs.existsSync(path.join(process.cwd(), 'pages', 'api'));
|
|
21
|
-
if (hasApiFolder) {
|
|
22
|
-
return { name: 'nextjs-pages', confidence: 0.9 };
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
// Check for Next.js App Router
|
|
26
|
-
if (fs.existsSync(path.join(process.cwd(), 'app'))) {
|
|
27
|
-
return { name: 'nextjs-app', confidence: 0.9 };
|
|
28
|
+
// Koa: middleware array
|
|
29
|
+
if (Array.isArray(app.middleware) && typeof app.use === 'function' && app.context) {
|
|
30
|
+
return { name: 'koa', confidence: 0.95 };
|
|
28
31
|
}
|
|
29
|
-
// Check for next.config
|
|
30
|
-
if (fs.existsSync(path.join(process.cwd(), 'next.config.js')) ||
|
|
31
|
-
fs.existsSync(path.join(process.cwd(), 'next.config.mjs'))) {
|
|
32
|
-
return { name: 'nextjs-pages', confidence: 0.7 };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch (e) {
|
|
36
|
-
// Ignore fs errors
|
|
37
32
|
}
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
// Fastify: inject method and route method
|
|
34
|
+
if (app &&
|
|
35
|
+
typeof app === 'object' &&
|
|
36
|
+
typeof app.inject === 'function' &&
|
|
37
|
+
typeof app.route === 'function') {
|
|
38
|
+
return { name: 'fastify', confidence: 1.0 };
|
|
42
39
|
}
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
// Hapi: server with route + start methods
|
|
41
|
+
if (app &&
|
|
42
|
+
typeof app === 'object' &&
|
|
43
|
+
typeof app.route === 'function' &&
|
|
44
|
+
typeof app.start === 'function' &&
|
|
45
|
+
typeof app.stop === 'function') {
|
|
46
|
+
return { name: 'hapi', confidence: 0.95 };
|
|
45
47
|
}
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// NestJS application
|
|
49
|
+
if (app &&
|
|
50
|
+
typeof app === 'object' &&
|
|
51
|
+
typeof app.getHttpServer === 'function' &&
|
|
52
|
+
typeof app.listen === 'function') {
|
|
53
|
+
return { name: 'nestjs', confidence: 0.95 };
|
|
50
54
|
}
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
// ── Filesystem detection ──────────────────────────────────────────────────
|
|
56
|
+
// Next.js App Router
|
|
57
|
+
if (fileExists('app') && (fileExists('app/layout.tsx') || fileExists('app/layout.js'))) {
|
|
58
|
+
return { name: 'nextjs-app', confidence: 0.92 };
|
|
53
59
|
}
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (http) {
|
|
58
|
-
return { name: 'node-http', confidence: 0.5 };
|
|
59
|
-
}
|
|
60
|
+
// Next.js Pages Router
|
|
61
|
+
if (fileExists('pages/api') || fileExists('src/pages/api')) {
|
|
62
|
+
return { name: 'nextjs-pages', confidence: 0.90 };
|
|
60
63
|
}
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
if (fileExists('next.config.js') ||
|
|
65
|
+
fileExists('next.config.mjs') ||
|
|
66
|
+
fileExists('next.config.ts')) {
|
|
67
|
+
return { name: 'nextjs-pages', confidence: 0.75 };
|
|
63
68
|
}
|
|
69
|
+
// ── Dependency-based detection (lowest confidence) ────────────────────────
|
|
70
|
+
if (tryResolve('express'))
|
|
71
|
+
return { name: 'express', confidence: 0.70 };
|
|
72
|
+
if (tryResolve('fastify'))
|
|
73
|
+
return { name: 'fastify', confidence: 0.70 };
|
|
74
|
+
if (tryResolve('koa'))
|
|
75
|
+
return { name: 'koa', confidence: 0.70 };
|
|
76
|
+
if (tryResolve('@hapi/hapi') || tryResolve('hapi'))
|
|
77
|
+
return { name: 'hapi', confidence: 0.70 };
|
|
78
|
+
if (tryResolve('@nestjs/core'))
|
|
79
|
+
return { name: 'nestjs', confidence: 0.70 };
|
|
80
|
+
if (tryResolve('vite'))
|
|
81
|
+
return { name: 'vite', confidence: 0.60 };
|
|
82
|
+
if (tryResolve('react-router-dom'))
|
|
83
|
+
return { name: 'react-router', confidence: 0.55 };
|
|
64
84
|
return { name: 'unknown', confidence: 0 };
|
|
65
85
|
}
|
package/dist/esm/export.js
CHANGED
|
@@ -54,6 +54,8 @@ export function enrichRoute(route, index) {
|
|
|
54
54
|
routeKey,
|
|
55
55
|
tag,
|
|
56
56
|
tags: route.tags?.length ? route.tags : [tag],
|
|
57
|
+
module: route.module,
|
|
58
|
+
service: route.service,
|
|
57
59
|
summary: route.summary || generateSummary(route.method, route.path),
|
|
58
60
|
description: route.description || generateDescription(route.method, route.path, tag),
|
|
59
61
|
authRequired,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Express analyzer —
|
|
2
|
+
* Express route analyzer — deep extraction of routes, parameters, and auth patterns
|
|
3
3
|
*/
|
|
4
|
-
// ─── Path
|
|
4
|
+
// ─── Path helpers ─────────────────────────────────────────────────────────────
|
|
5
5
|
function normalizePath(raw) {
|
|
6
6
|
return raw
|
|
7
7
|
.replace(/\\\//g, '/')
|
|
@@ -10,11 +10,10 @@ function normalizePath(raw) {
|
|
|
10
10
|
.replace(/\\\w/g, '')
|
|
11
11
|
.replace(/[\^\$\?\\]/g, '')
|
|
12
12
|
.replace(/\/+/g, '/')
|
|
13
|
-
.replace(/\/$/, '')
|
|
14
|
-
|| '/';
|
|
13
|
+
.replace(/\/$/, '') || '/';
|
|
15
14
|
}
|
|
16
15
|
function tagFromPath(path) {
|
|
17
|
-
const skip = new Set(['api', 'v1', 'v2', 'v3', 'v4']);
|
|
16
|
+
const skip = new Set(['api', 'v1', 'v2', 'v3', 'v4', 'v5']);
|
|
18
17
|
const meaningful = path
|
|
19
18
|
.split('/')
|
|
20
19
|
.filter(Boolean)
|
|
@@ -24,18 +23,18 @@ function tagFromPath(path) {
|
|
|
24
23
|
function generateSummary(method, path) {
|
|
25
24
|
const parts = path.split('/').filter(s => s && !s.startsWith(':'));
|
|
26
25
|
const action = parts[parts.length - 1] || '';
|
|
27
|
-
const
|
|
26
|
+
const res = parts[parts.length - 2] || '';
|
|
28
27
|
const map = {
|
|
29
|
-
GET: action ? `Get ${action}` : `List ${
|
|
30
|
-
POST: action ? `Create ${action}` : `Create ${
|
|
31
|
-
PUT: action ? `Update ${action}` : `
|
|
32
|
-
PATCH: action ? `Patch ${action}` : `Patch ${
|
|
33
|
-
DELETE: action ? `Delete ${action}` : `Delete ${
|
|
28
|
+
GET: action ? `Get ${action}` : `List ${res}`,
|
|
29
|
+
POST: action ? `Create ${action}` : `Create ${res}`,
|
|
30
|
+
PUT: action ? `Update ${action}` : `Replace ${res}`,
|
|
31
|
+
PATCH: action ? `Patch ${action}` : `Patch ${res}`,
|
|
32
|
+
DELETE: action ? `Delete ${action}` : `Delete ${res}`,
|
|
34
33
|
};
|
|
35
34
|
const base = map[method] || `${method} ${path}`;
|
|
36
35
|
return (base.charAt(0).toUpperCase() + base.slice(1)).replace(/[-_]/g, ' ');
|
|
37
36
|
}
|
|
38
|
-
// ─── Main
|
|
37
|
+
// ─── Main export ─────────────────────────────────────────────────────────────
|
|
39
38
|
export function analyzeExpress(app) {
|
|
40
39
|
const routes = [];
|
|
41
40
|
const seen = new Set();
|
|
@@ -50,36 +49,45 @@ export function analyzeExpress(app) {
|
|
|
50
49
|
console.warn('⚠️ No routes found — ensure routes are registered before analyze()');
|
|
51
50
|
return routes;
|
|
52
51
|
}
|
|
52
|
+
walkStack(stack, '', routes, seen);
|
|
53
|
+
return routes;
|
|
54
|
+
}
|
|
55
|
+
function walkStack(stack, basePath, out, seen) {
|
|
53
56
|
for (const layer of stack) {
|
|
54
57
|
if (layer.route) {
|
|
55
|
-
const r = buildRoute(layer.route,
|
|
58
|
+
const r = buildRoute(layer.route, basePath);
|
|
56
59
|
if (r && !seen.has(r.method + r.path)) {
|
|
57
|
-
|
|
60
|
+
out.push(r);
|
|
58
61
|
seen.add(r.method + r.path);
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
else if (layer.handle?.stack) {
|
|
62
|
-
|
|
65
|
+
// Router or sub-app mounted at a prefix
|
|
66
|
+
let prefix = '';
|
|
63
67
|
if (layer.regexp?.source)
|
|
64
|
-
|
|
68
|
+
prefix = normalizePath(layer.regexp.source);
|
|
65
69
|
else if (layer.path)
|
|
66
|
-
|
|
70
|
+
prefix = normalizePath(layer.path);
|
|
67
71
|
else if (typeof layer.route === 'string')
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const r = buildRoute(sub.route, base);
|
|
72
|
-
if (r && !seen.has(r.method + r.path)) {
|
|
73
|
-
routes.push(r);
|
|
74
|
-
seen.add(r.method + r.path);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
72
|
+
prefix = normalizePath(layer.route);
|
|
73
|
+
const combined = combinePaths(basePath, prefix);
|
|
74
|
+
walkStack(layer.handle.stack, combined, out, seen);
|
|
78
75
|
}
|
|
79
76
|
}
|
|
80
|
-
|
|
77
|
+
}
|
|
78
|
+
function combinePaths(base, suffix) {
|
|
79
|
+
const b = base === '/' ? '' : base;
|
|
80
|
+
const s = suffix === '/' ? '' : suffix;
|
|
81
|
+
return b + s || '/';
|
|
81
82
|
}
|
|
82
83
|
// ─── Route builder ────────────────────────────────────────────────────────────
|
|
84
|
+
const AUTH_NAMES = new Set([
|
|
85
|
+
'authenticateToken', 'authenticate', 'verifyToken', 'requireAuth',
|
|
86
|
+
'auth', 'protect', 'isAuth', 'checkAuth', 'jwtAuth', 'bearerAuth',
|
|
87
|
+
'isAuthenticated', 'requireLogin', 'ensureAuth', 'verifyJWT',
|
|
88
|
+
'checkToken', 'authMiddleware', 'jwt', 'bearer', 'apiKeyAuth',
|
|
89
|
+
'passport.authenticate',
|
|
90
|
+
]);
|
|
83
91
|
function buildRoute(route, basePath) {
|
|
84
92
|
if (!route.path)
|
|
85
93
|
return null;
|
|
@@ -100,59 +108,44 @@ function buildRoute(route, basePath) {
|
|
|
100
108
|
const middleware = stack
|
|
101
109
|
.map((l) => l.name)
|
|
102
110
|
.filter((n) => n && n !== '<anonymous>' && n !== 'bound dispatch' && n !== 'bound ');
|
|
103
|
-
// ── Join all handler source
|
|
111
|
+
// ── Join all handler source ────────────────────────────────────────────────
|
|
104
112
|
const src = stack.map((l) => l.handle?.toString() ?? '').join('\n');
|
|
105
|
-
// ── Path params
|
|
113
|
+
// ── Path params from route pattern ─────────────────────────────────────────
|
|
106
114
|
const pathParams = unique([...fullPath.matchAll(/:(\w+)/g)].map(m => m[1]));
|
|
107
115
|
// ── Query params ───────────────────────────────────────────────────────────
|
|
108
116
|
const queryParams = unique([
|
|
109
|
-
// req.query.foo
|
|
110
117
|
...matchAll(src, /req\.query\.([a-zA-Z0-9_$]+)/g),
|
|
111
|
-
// { foo, bar } = req.query / { foo: renamed } = req.query
|
|
112
118
|
...destructured(src, /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*req\.query/g),
|
|
113
|
-
// req.query['foo']
|
|
114
119
|
...matchAll(src, /req\.query\[['"]([a-zA-Z0-9_$]+)['"]\]/g),
|
|
115
120
|
]);
|
|
116
121
|
// ── Request body fields ────────────────────────────────────────────────────
|
|
117
122
|
const requestBodyFields = unique([
|
|
118
|
-
// req.body.foo
|
|
119
123
|
...matchAll(src, /req\.body\.([a-zA-Z0-9_$]+)/g),
|
|
120
|
-
// { foo, bar: alias, baz = default } = req.body
|
|
121
124
|
...destructured(src, /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*req\.body/g),
|
|
122
|
-
// req.body['foo']
|
|
123
125
|
...matchAll(src, /req\.body\[['"]([a-zA-Z0-9_$]+)['"]\]/g),
|
|
124
|
-
// body.foo (when `const body = req.body`)
|
|
125
126
|
...bodyAliasFields(src),
|
|
126
|
-
// Zod: z.object({ field: z.string() }).parse(req.body)
|
|
127
127
|
...zodFields(src),
|
|
128
|
-
// Joi: Joi.object({ field: Joi.string() }).validate(req.body)
|
|
129
128
|
...joiFields(src),
|
|
129
|
+
...typeInterfaceFields(src),
|
|
130
130
|
]);
|
|
131
131
|
// ── Header params ──────────────────────────────────────────────────────────
|
|
132
132
|
const headerParams = unique([
|
|
133
133
|
...matchAll(src, /req\.headers\[['"]([a-zA-Z0-9_$-]+)['"]\]/g),
|
|
134
134
|
...matchAll(src, /req\.get\s*\(\s*['"]([a-zA-Z0-9_$-]+)['"]\s*\)/g),
|
|
135
|
-
// req.headers.authorization, req.headers['x-api-key'], etc.
|
|
136
135
|
...matchAll(src, /req\.headers\.([a-zA-Z0-9_$]+)/g),
|
|
137
136
|
]);
|
|
138
|
-
// ── Response status codes
|
|
137
|
+
// ── Response status codes ──────────────────────────────────────────────────
|
|
139
138
|
const responseStatuses = unique([...src.matchAll(/res\.status\s*\(\s*(\d{3})\s*\)/g)].map(m => parseInt(m[1], 10)));
|
|
140
139
|
// ── Auth detection ─────────────────────────────────────────────────────────
|
|
141
|
-
const AUTH_NAMES = new Set([
|
|
142
|
-
'authenticateToken', 'authenticate', 'verifyToken', 'requireAuth', 'auth',
|
|
143
|
-
'protect', 'isAuth', 'checkAuth', 'jwtAuth', 'bearerAuth', 'isAuthenticated',
|
|
144
|
-
'requireLogin', 'ensureAuth', 'verifyJWT', 'checkToken', 'authMiddleware',
|
|
145
|
-
'passport.authenticate', 'jwt', 'bearer',
|
|
146
|
-
]);
|
|
147
140
|
const hasAuth = middleware.some(m => AUTH_NAMES.has(m) || [...AUTH_NAMES].some(a => m.toLowerCase().includes(a.toLowerCase()))) ||
|
|
148
|
-
/req\.user|req\.auth|verifyToken|jwt\.verify|passport\.authenticate/.test(src);
|
|
141
|
+
/req\.user|req\.auth|req\.currentUser|verifyToken|jwt\.verify|passport\.authenticate/.test(src);
|
|
149
142
|
const method = methods[0];
|
|
150
143
|
const tag = tagFromPath(fullPath);
|
|
151
144
|
const summary = generateSummary(method, fullPath);
|
|
152
145
|
const operationId = `${method.toLowerCase()}_${fullPath
|
|
153
146
|
.replace(/[^a-zA-Z0-9]/g, '_')
|
|
154
147
|
.replace(/_+/g, '_')
|
|
155
|
-
.replace(/^_|_
|
|
148
|
+
.replace(/^_|_$/, '')}`;
|
|
156
149
|
return {
|
|
157
150
|
method,
|
|
158
151
|
path: fullPath,
|
|
@@ -172,49 +165,33 @@ function buildRoute(route, basePath) {
|
|
|
172
165
|
};
|
|
173
166
|
}
|
|
174
167
|
// ─── Extraction helpers ───────────────────────────────────────────────────────
|
|
175
|
-
/** Run a global regex and return capture group 1 for every match. */
|
|
176
168
|
function matchAll(src, re) {
|
|
177
169
|
return [...src.matchAll(re)].map(m => m[1]).filter(Boolean);
|
|
178
170
|
}
|
|
179
|
-
/**
|
|
180
|
-
* Extract field names from all destructuring patterns like:
|
|
181
|
-
* const { a, b: alias, c = default, d: alias2 = default } = req.body
|
|
182
|
-
* Runs ALL occurrences of the given regex, not just the first.
|
|
183
|
-
*/
|
|
184
171
|
function destructured(src, re) {
|
|
185
172
|
const results = [];
|
|
186
173
|
for (const match of src.matchAll(re)) {
|
|
187
174
|
match[1]
|
|
188
175
|
.split(',')
|
|
189
|
-
.map(s => s.trim())
|
|
190
|
-
// handle "alias: original" — we want the local binding (left side)
|
|
191
|
-
.map(s => s.split(':')[0].trim())
|
|
192
|
-
// strip default value
|
|
193
|
-
.map(s => s.split('=')[0].trim())
|
|
176
|
+
.map(s => s.trim().split(':')[0].trim().split('=')[0].trim())
|
|
194
177
|
.filter(Boolean)
|
|
195
178
|
.forEach(f => results.push(f));
|
|
196
179
|
}
|
|
197
180
|
return results;
|
|
198
181
|
}
|
|
199
|
-
/**
|
|
200
|
-
* When a developer writes `const body = req.body` and then accesses `body.foo`.
|
|
201
|
-
*/
|
|
202
182
|
function bodyAliasFields(src) {
|
|
203
|
-
const
|
|
204
|
-
if (!
|
|
183
|
+
const m = src.match(/(?:const|let|var)\s+(\w+)\s*=\s*req\.body\b/);
|
|
184
|
+
if (!m)
|
|
205
185
|
return [];
|
|
206
|
-
const alias =
|
|
186
|
+
const alias = m[1];
|
|
207
187
|
if (alias === 'body')
|
|
208
188
|
return matchAll(src, /body\.([a-zA-Z0-9_$]+)/g);
|
|
209
189
|
const re = new RegExp(`\\b${alias}\\.([a-zA-Z0-9_$]+)`, 'g');
|
|
210
190
|
return matchAll(src, re);
|
|
211
191
|
}
|
|
212
|
-
/** Extract field names from Zod object schemas. */
|
|
213
192
|
function zodFields(src) {
|
|
214
193
|
const results = [];
|
|
215
|
-
|
|
216
|
-
const zodRe = /z\.object\s*\(\s*\{([^}]+)\}/g;
|
|
217
|
-
for (const match of src.matchAll(zodRe)) {
|
|
194
|
+
for (const match of src.matchAll(/z\.object\s*\(\s*\{([^}]+)\}/g)) {
|
|
218
195
|
match[1]
|
|
219
196
|
.split(',')
|
|
220
197
|
.map(s => s.trim().split(':')[0].trim())
|
|
@@ -223,12 +200,9 @@ function zodFields(src) {
|
|
|
223
200
|
}
|
|
224
201
|
return results;
|
|
225
202
|
}
|
|
226
|
-
/** Extract field names from Joi object schemas. */
|
|
227
203
|
function joiFields(src) {
|
|
228
204
|
const results = [];
|
|
229
|
-
|
|
230
|
-
const joiRe = /(?:Joi|yup)\.object\s*\(?\s*(?:\.shape\s*\()?\s*\{([^}]+)\}/g;
|
|
231
|
-
for (const match of src.matchAll(joiRe)) {
|
|
205
|
+
for (const match of src.matchAll(/(?:Joi|yup)\.object\s*\(?\s*(?:\.shape\s*\()?\s*\{([^}]+)\}/g)) {
|
|
232
206
|
match[1]
|
|
233
207
|
.split(',')
|
|
234
208
|
.map(s => s.trim().split(':')[0].trim())
|
|
@@ -237,6 +211,38 @@ function joiFields(src) {
|
|
|
237
211
|
}
|
|
238
212
|
return results;
|
|
239
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Detects TypeScript interface/type fields used as request body type parameters.
|
|
216
|
+
* Handles patterns like:
|
|
217
|
+
* const { name, email } = req.body as CreateUserDto
|
|
218
|
+
* function handler(req: Request<{}, {}, CreateUserBody>) { ... }
|
|
219
|
+
* This runs the interface declaration through a lightweight regex rather than a
|
|
220
|
+
* full TS AST to keep the package zero-dependency.
|
|
221
|
+
*/
|
|
222
|
+
function typeInterfaceFields(src) {
|
|
223
|
+
const results = [];
|
|
224
|
+
// Extract type name used as body generic: Request<P, ResBody, BodyType>
|
|
225
|
+
const bodyTypeNames = new Set();
|
|
226
|
+
for (const m of src.matchAll(/Request\s*<[^,]*,[^,]*,\s*([A-Z][a-zA-Z0-9_]*)\s*[,>]/g)) {
|
|
227
|
+
bodyTypeNames.add(m[1]);
|
|
228
|
+
}
|
|
229
|
+
// `req.body as SomeType`
|
|
230
|
+
for (const m of src.matchAll(/req\.body\s+as\s+([A-Z][a-zA-Z0-9_]*)/g)) {
|
|
231
|
+
bodyTypeNames.add(m[1]);
|
|
232
|
+
}
|
|
233
|
+
for (const typeName of bodyTypeNames) {
|
|
234
|
+
// Find interface { field: type; ... } or type alias { field: type }
|
|
235
|
+
const blockRe = new RegExp(`(?:interface|type)\\s+${typeName}\\s*(?:extends[^{]+)?\\{([^}]+)\\}`, 'g');
|
|
236
|
+
for (const match of src.matchAll(blockRe)) {
|
|
237
|
+
match[1]
|
|
238
|
+
.split(/[;,\n]/)
|
|
239
|
+
.map(line => line.trim().split(/[?:]/)[0].trim())
|
|
240
|
+
.filter(f => /^[a-zA-Z_$]/.test(f))
|
|
241
|
+
.forEach(f => results.push(f));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return results;
|
|
245
|
+
}
|
|
240
246
|
function unique(items) {
|
|
241
247
|
return Array.from(new Set(items.filter(Boolean)));
|
|
242
248
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { analyzeExpress } from './express.js';
|
|
2
|
+
import { detectNestModules } from '../modules/nestjs.js';
|
|
3
|
+
import { detectExpressModules, annotateRoutesWithModules } from '../modules/express.js';
|
|
4
|
+
/**
|
|
5
|
+
* Analyze a NestJS application by extracting the underlying Express (or Fastify)
|
|
6
|
+
* HTTP adapter and reusing the existing Express route-walker.
|
|
7
|
+
*
|
|
8
|
+
* NestJS wraps an inner Express app under app.getHttpServer()._events.request.
|
|
9
|
+
* When that's available we get full runtime accuracy. Otherwise we warn and
|
|
10
|
+
* fall back to an empty array so the CLI can report the limitation.
|
|
11
|
+
*/
|
|
12
|
+
export async function analyzeNestJS(app, rootDir) {
|
|
13
|
+
if (!app || typeof app.getHttpServer !== 'function') {
|
|
14
|
+
console.warn('⚠️ APIForge: NestJS app instance required. Pass the Nest application object.');
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
const httpServer = app.getHttpServer();
|
|
18
|
+
// NestJS wraps Express under _events.request when using the default adapter
|
|
19
|
+
const innerApp = httpServer?._events?.request ||
|
|
20
|
+
httpServer?._events?.['request'] ||
|
|
21
|
+
null;
|
|
22
|
+
if (!innerApp || typeof innerApp !== 'function') {
|
|
23
|
+
console.warn('⚠️ APIForge: Could not access inner Express adapter from NestJS app.');
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const routes = await analyzeExpress(innerApp);
|
|
27
|
+
if (routes.length === 0)
|
|
28
|
+
return [];
|
|
29
|
+
// Detect module structure and annotate each route
|
|
30
|
+
const dir = rootDir || process.cwd();
|
|
31
|
+
const modules = detectNestModules(dir).length > 0
|
|
32
|
+
? detectNestModules(dir)
|
|
33
|
+
: detectExpressModules(dir);
|
|
34
|
+
return annotateRoutesWithModules(routes, modules);
|
|
35
|
+
}
|