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