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.
@@ -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 which framework is being used
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
- // Express detection
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
- // Next.js detection
18
- try {
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
- // Vite detection
42
- try {
43
- require.resolve('vite');
44
- return { name: 'vite', confidence: 0.8 };
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
- catch (e) {
47
- // Vite not installed
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
- // React Router detection
50
- try {
51
- require.resolve('react-router-dom');
52
- return { name: 'react-router', confidence: 0.7 };
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
- catch (e) {
55
- // React Router not installed
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
- // Node HTTP detection
58
- try {
59
- const http = require('http');
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
- catch (e) {
65
- // HTTP module not available
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
  }
@@ -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 — accurate route + payload extraction
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 normalisation ───────────────────────────────────────────────────────
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 resource = parts[parts.length - 2] || '';
29
+ const res = parts[parts.length - 2] || '';
31
30
  const map = {
32
- GET: action ? `Get ${action}` : `List ${resource}`,
33
- POST: action ? `Create ${action}` : `Create ${resource}`,
34
- PUT: action ? `Update ${action}` : `Update ${resource}`,
35
- PATCH: action ? `Patch ${action}` : `Patch ${resource}`,
36
- DELETE: action ? `Delete ${action}` : `Delete ${resource}`,
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 analyser ────────────────────────────────────────────────────────────
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
- routes.push(r);
63
+ out.push(r);
61
64
  seen.add(r.method + r.path);
62
65
  }
63
66
  }
64
67
  else if (layer.handle?.stack) {
65
- let base = '';
68
+ // Router or sub-app mounted at a prefix
69
+ let prefix = '';
66
70
  if (layer.regexp?.source)
67
- base = normalizePath(layer.regexp.source);
71
+ prefix = normalizePath(layer.regexp.source);
68
72
  else if (layer.path)
69
- base = normalizePath(layer.path);
73
+ prefix = normalizePath(layer.path);
70
74
  else if (typeof layer.route === 'string')
71
- base = normalizePath(layer.route);
72
- for (const sub of layer.handle.stack) {
73
- if (sub.route) {
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
- return routes;
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 text ───────────────────────────────────────────
114
+ // ── Join all handler source ────────────────────────────────────────────────
107
115
  const src = stack.map((l) => l.handle?.toString() ?? '').join('\n');
108
- // ── Path params (from route pattern) ──────────────────────────────────────
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 from `res.status(nnn)` ──────────────────────────
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(/^_|_$/g, '')}`;
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 aliasMatch = src.match(/(?:const|let|var)\s+(\w+)\s*=\s*req\.body\b/);
207
- if (!aliasMatch)
186
+ const m = src.match(/(?:const|let|var)\s+(\w+)\s*=\s*req\.body\b/);
187
+ if (!m)
208
188
  return [];
209
- const alias = aliasMatch[1];
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
- // z.object({ field: z.xxx(), ... }).parse(req.body) or .safeParse
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
- // Joi.object({ field: Joi.xxx() }) or yup.object().shape({ field: ... })
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
+ }