apiforge-analyzer 1.1.0 → 1.2.1
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/dist/cjs/cli.js +425 -255
- package/dist/cjs/detect.js +68 -48
- package/dist/cjs/frameworks/express.js +80 -74
- package/dist/cjs/index.js +118 -58
- package/dist/cjs/uploader.js +49 -11
- package/dist/esm/cli.js +424 -257
- package/dist/esm/detect.js +68 -48
- package/dist/esm/frameworks/express.js +80 -74
- package/dist/esm/index.js +120 -60
- package/dist/esm/uploader.js +47 -11
- package/dist/types/detect.d.ts +3 -5
- package/dist/types/frameworks/express.d.ts +2 -2
- package/dist/types/index.d.ts +1 -4
- package/dist/types/uploader.d.ts +10 -2
- package/package.json +1 -1
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
|
}
|
|
@@ -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
|
}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* APIForge Analyzer — main entry point (dual ESM/CJS)
|
|
3
3
|
*/
|
|
4
|
+
import { writeFileSync, readFileSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
4
6
|
import { analyzeExpress } from './frameworks/express.js';
|
|
5
7
|
import { analyzeNextPages } from './frameworks/nextjs-pages.js';
|
|
6
8
|
import { analyzeNextApp } from './frameworks/nextjs-app.js';
|
|
@@ -8,84 +10,143 @@ import { analyzeVite } from './frameworks/vite.js';
|
|
|
8
10
|
import { analyzeReactRouter } from './frameworks/react-router.js';
|
|
9
11
|
import { detectFramework } from './detect.js';
|
|
10
12
|
import { exportToJSON } from './export.js';
|
|
11
|
-
import { uploadRoutes } from './uploader.js';
|
|
12
|
-
//
|
|
13
|
-
const analysisState = new Map();
|
|
14
|
-
const DEBOUNCE_TIME = 5000;
|
|
13
|
+
import { uploadRoutes, writeStatusFile } from './uploader.js';
|
|
14
|
+
// ─── Env file loader — no dotenv dependency ───────────────────────────────────
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Read a single variable from a .env-style file.
|
|
17
|
+
* Plain Node/Express doesn't auto-load .env.local, so we do it ourselves.
|
|
18
|
+
* The package never requires dotenv or any other dep for this.
|
|
17
19
|
*/
|
|
20
|
+
function readEnvFile(varName) {
|
|
21
|
+
const candidates = [
|
|
22
|
+
'.env.local',
|
|
23
|
+
'.env.development.local',
|
|
24
|
+
'.env.development',
|
|
25
|
+
'.env',
|
|
26
|
+
];
|
|
27
|
+
for (const file of candidates) {
|
|
28
|
+
try {
|
|
29
|
+
const content = readFileSync(join(process.cwd(), file), 'utf-8');
|
|
30
|
+
for (const line of content.split('\n')) {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (trimmed.startsWith('#') || !trimmed.includes('='))
|
|
33
|
+
continue;
|
|
34
|
+
const eqIdx = trimmed.indexOf('=');
|
|
35
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
36
|
+
if (key !== varName)
|
|
37
|
+
continue;
|
|
38
|
+
return trimmed.slice(eqIdx + 1).trim().replace(/^['"]|['"]$/g, '');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
// ─── File-based debounce (survives nodemon restarts) ──────────────────────────
|
|
46
|
+
const DEBOUNCE_FILE = '.apiforge-last-run.json';
|
|
47
|
+
const DEBOUNCE_MS = 30000; // 30 s
|
|
48
|
+
function readLastRunMs() {
|
|
49
|
+
try {
|
|
50
|
+
const raw = readFileSync(join(process.cwd(), DEBOUNCE_FILE), 'utf-8');
|
|
51
|
+
return JSON.parse(raw).timestamp ?? 0;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function stampLastRun() {
|
|
58
|
+
try {
|
|
59
|
+
writeFileSync(join(process.cwd(), DEBOUNCE_FILE), JSON.stringify({ timestamp: Date.now() }), 'utf-8');
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
}
|
|
63
|
+
// ─── Main analyze ─────────────────────────────────────────────────────────────
|
|
18
64
|
export async function analyze(app, options) {
|
|
19
|
-
//
|
|
65
|
+
// Never run in production
|
|
20
66
|
if (options.skipInProduction !== false && process.env.NODE_ENV === 'production') {
|
|
21
|
-
console.log('⏭️ Skipping analysis in production');
|
|
22
67
|
return [];
|
|
23
68
|
}
|
|
24
|
-
//
|
|
69
|
+
// File-based debounce — survives nodemon restarts
|
|
25
70
|
if (options.runOnce !== false) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return cached.routes;
|
|
71
|
+
const elapsed = Date.now() - readLastRunMs();
|
|
72
|
+
if (elapsed < DEBOUNCE_MS) {
|
|
73
|
+
console.log(`⚡ APIForge: skipped (last run ${Math.round(elapsed / 1000)}s ago)`);
|
|
74
|
+
return [];
|
|
31
75
|
}
|
|
76
|
+
stampLastRun();
|
|
77
|
+
}
|
|
78
|
+
// ── Resolve API key ─────────────────────────────────────────────────────────
|
|
79
|
+
// Priority: explicit option → process.env → .env.local/.env file
|
|
80
|
+
// This means the package works even if the user's server doesn't load dotenv.
|
|
81
|
+
if (!options.apiKey) {
|
|
82
|
+
options.apiKey =
|
|
83
|
+
process.env.APIFORGE_API_KEY ||
|
|
84
|
+
readEnvFile('APIFORGE_API_KEY');
|
|
32
85
|
}
|
|
33
|
-
console.log(`🔍
|
|
34
|
-
//
|
|
86
|
+
console.log(`🔍 APIForge: analyzing ${options.projectName}…`);
|
|
87
|
+
// Writing saveJson files into the watched dir triggers nodemon restart loops
|
|
35
88
|
if (options.upload !== false && options.apiKey) {
|
|
36
89
|
options.saveJson = false;
|
|
37
90
|
}
|
|
91
|
+
// ── Early failure: upload desired but no API key anywhere ──────────────────
|
|
92
|
+
if (options.upload !== false && !options.apiKey) {
|
|
93
|
+
const msg = 'APIFORGE_API_KEY not found in options, process.env, or .env.local';
|
|
94
|
+
console.error(`❌ APIForge: ${msg}`);
|
|
95
|
+
writeStatusFile('error', { error: msg });
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
38
98
|
let routes = [];
|
|
39
99
|
let framework = options.framework;
|
|
40
|
-
// Auto-detect framework if not specified
|
|
41
100
|
if (!framework) {
|
|
42
|
-
|
|
43
|
-
framework = detected.name;
|
|
101
|
+
framework = detectFramework(app).name;
|
|
44
102
|
console.log(`📦 Detected framework: ${framework}`);
|
|
45
103
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
104
|
+
try {
|
|
105
|
+
switch (framework) {
|
|
106
|
+
case 'express':
|
|
107
|
+
if (!app) {
|
|
108
|
+
const msg = 'Express app instance required — pass your app object to analyze()';
|
|
109
|
+
console.error(`❌ ${msg}`);
|
|
110
|
+
writeStatusFile('error', { error: msg });
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
routes = await analyzeExpress(app);
|
|
114
|
+
break;
|
|
115
|
+
case 'nextjs-pages':
|
|
116
|
+
routes = await analyzeNextPages(options.nextDir || 'pages/api');
|
|
117
|
+
break;
|
|
118
|
+
case 'nextjs-app':
|
|
119
|
+
routes = await analyzeNextApp(options.nextDir || 'app');
|
|
120
|
+
break;
|
|
121
|
+
case 'vite':
|
|
122
|
+
routes = await analyzeVite(options.nextDir || 'vite.config.js');
|
|
123
|
+
break;
|
|
124
|
+
case 'react-router':
|
|
125
|
+
routes = await analyzeReactRouter(options.nextDir || 'src/routes.tsx');
|
|
126
|
+
break;
|
|
127
|
+
default:
|
|
128
|
+
const msg = `Framework "${framework}" not supported yet (express, nextjs-pages, nextjs-app, vite, react-router)`;
|
|
129
|
+
console.warn(`⚠️ APIForge: ${msg}`);
|
|
130
|
+
writeStatusFile('error', { error: msg });
|
|
51
131
|
return [];
|
|
52
|
-
|
|
53
|
-
routes = await analyzeExpress(app);
|
|
54
|
-
break;
|
|
55
|
-
case 'nextjs-pages':
|
|
56
|
-
routes = await analyzeNextPages(options.nextDir || 'pages/api');
|
|
57
|
-
break;
|
|
58
|
-
case 'nextjs-app':
|
|
59
|
-
routes = await analyzeNextApp(options.nextDir || 'app');
|
|
60
|
-
break;
|
|
61
|
-
case 'vite':
|
|
62
|
-
routes = await analyzeVite(options.nextDir || 'vite.config.js');
|
|
63
|
-
break;
|
|
64
|
-
case 'react-router':
|
|
65
|
-
routes = await analyzeReactRouter(options.nextDir || 'src/routes.tsx');
|
|
66
|
-
break;
|
|
67
|
-
case 'node-http':
|
|
68
|
-
case 'unknown':
|
|
69
|
-
default:
|
|
70
|
-
console.warn(`⚠️ Framework "${framework}" not supported yet`);
|
|
71
|
-
console.log('💡 Supported: express, nextjs-pages, nextjs-app, vite, react-router');
|
|
72
|
-
console.log('💡 Tip: Specify framework manually: { framework: "express" }');
|
|
73
|
-
return [];
|
|
132
|
+
}
|
|
74
133
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
routes
|
|
81
|
-
});
|
|
134
|
+
catch (err) {
|
|
135
|
+
const msg = err.message || 'Unknown error during route extraction';
|
|
136
|
+
console.error(`❌ APIForge analysis error: ${msg}`);
|
|
137
|
+
writeStatusFile('error', { error: msg });
|
|
138
|
+
return [];
|
|
82
139
|
}
|
|
83
|
-
|
|
140
|
+
if (routes.length === 0) {
|
|
141
|
+
const msg = 'No routes found — ensure routes are registered before analyze() is called';
|
|
142
|
+
console.warn(`⚠️ APIForge: ${msg}`);
|
|
143
|
+
writeStatusFile('error', { error: msg });
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
console.log(`✅ Found ${routes.length} routes`);
|
|
84
147
|
if (options.saveJson) {
|
|
85
|
-
|
|
86
|
-
await exportToJSON(routes, options.projectName, filePath, options.baseUrl);
|
|
148
|
+
await exportToJSON(routes, options.projectName, options.jsonPath || 'api-export.json', options.baseUrl);
|
|
87
149
|
}
|
|
88
|
-
// Upload to APIForge
|
|
89
150
|
if (options.upload !== false && options.apiKey) {
|
|
90
151
|
await uploadRoutes(routes, {
|
|
91
152
|
projectName: options.projectName,
|
|
@@ -96,12 +157,11 @@ export async function analyze(app, options) {
|
|
|
96
157
|
}
|
|
97
158
|
return routes;
|
|
98
159
|
}
|
|
99
|
-
// Re-
|
|
160
|
+
// Re-exports for library consumers
|
|
100
161
|
export { analyzeExpress, analyzeNextPages, analyzeNextApp, analyzeVite, analyzeReactRouter };
|
|
101
162
|
export { exportToJSON } from './export.js';
|
|
102
163
|
export { uploadRoutes } from './uploader.js';
|
|
103
164
|
export { detectFramework } from './detect.js';
|
|
104
|
-
// Default export for CommonJS
|
|
105
165
|
export default {
|
|
106
166
|
analyze,
|
|
107
167
|
analyzeExpress,
|
|
@@ -111,5 +171,5 @@ export default {
|
|
|
111
171
|
analyzeReactRouter,
|
|
112
172
|
exportToJSON,
|
|
113
173
|
uploadRoutes,
|
|
114
|
-
detectFramework
|
|
174
|
+
detectFramework,
|
|
115
175
|
};
|