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.
@@ -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
  }
@@ -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
  }
package/dist/cjs/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  /**
3
- * Main analyzer entry point with dual module support
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
- // Global state for debouncing
22
- const analysisState = new Map();
23
- const DEBOUNCE_TIME = 5000;
23
+ // ─── Env file loader — no dotenv dependency ───────────────────────────────────
24
24
  /**
25
- * Main analyze function with auto-detection
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
- // Skip in production
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
- // Debounce check
78
+ // File-based debounce — survives nodemon restarts
34
79
  if (options.runOnce !== false) {
35
- const cached = analysisState.get(options.projectName);
36
- const now = Date.now();
37
- if (cached && (now - cached.timestamp) < DEBOUNCE_TIME) {
38
- console.log(`⚡ Analysis skipped - already ran ${Math.round((now - cached.timestamp) / 1000)}s ago`);
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(`🔍 Analyzing ${options.projectName}...`);
43
- // CRITICAL: Prevent restart loops - force saveJson=false when uploading
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
- const detected = (0, detect_js_1.detectFramework)(app);
52
- framework = detected.name;
110
+ framework = (0, detect_js_1.detectFramework)(app).name;
53
111
  console.log(`📦 Detected framework: ${framework}`);
54
112
  }
55
- // Analyze based on framework
56
- switch (framework) {
57
- case 'express':
58
- if (!app) {
59
- console.error('❌ Express app instance required');
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
- console.log(`✅ Found ${routes.length} routes`);
85
- // Cache results
86
- if (options.runOnce !== false) {
87
- analysisState.set(options.projectName, {
88
- timestamp: Date.now(),
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
- // Save JSON (only if not uploading)
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
- const filePath = options.jsonPath || 'api-export.json';
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
  };