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