apiforge-analyzer 1.3.2 → 1.3.3

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/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * APIForge Analyzer — main entry point (dual ESM/CJS)
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.detectArchitectureType = exports.detectFramework = exports.uploadRoutes = exports.exportToJSON = exports.analyzeNestJS = exports.analyzeReactRouter = exports.analyzeVite = exports.analyzeNextApp = exports.analyzeNextPages = exports.analyzeExpress = void 0;
6
+ exports.detectArchitectureType = exports.detectFramework = exports.uploadRoutes = exports.exportToJSON = exports.analyzeFastify = exports.analyzeNestJS = exports.analyzeReactRouter = exports.analyzeVite = exports.analyzeNextApp = exports.analyzeNextPages = exports.analyzeExpress = void 0;
7
7
  exports.analyze = analyze;
8
8
  const fs_1 = require("fs");
9
9
  const path_1 = require("path");
@@ -19,6 +19,8 @@ const react_router_js_1 = require("./frameworks/react-router.js");
19
19
  Object.defineProperty(exports, "analyzeReactRouter", { enumerable: true, get: function () { return react_router_js_1.analyzeReactRouter; } });
20
20
  const nestjs_js_1 = require("./frameworks/nestjs.js");
21
21
  Object.defineProperty(exports, "analyzeNestJS", { enumerable: true, get: function () { return nestjs_js_1.analyzeNestJS; } });
22
+ const fastify_js_1 = require("./frameworks/fastify.js");
23
+ Object.defineProperty(exports, "analyzeFastify", { enumerable: true, get: function () { return fastify_js_1.analyzeFastify; } });
22
24
  const detect_js_1 = require("./detect.js");
23
25
  const export_js_1 = require("./export.js");
24
26
  const uploader_js_1 = require("./uploader.js");
@@ -117,6 +119,15 @@ async function analyze(app, options) {
117
119
  framework = (0, detect_js_1.detectFramework)(app).name;
118
120
  console.log(`📦 Detected framework: ${framework}`);
119
121
  }
122
+ // Reject loudly on an unsupported framework instead of falling through to
123
+ // `return []` — that return value is indistinguishable from "no routes",
124
+ // "no API key", "debounced", etc. A caller needs to be able to tell them apart.
125
+ if (!detect_js_1.SUPPORTED_FRAMEWORKS.has(framework)) {
126
+ const msg = `Framework "${framework}" not supported yet (${[...detect_js_1.SUPPORTED_FRAMEWORKS].join(', ')})`;
127
+ console.error(`❌ APIForge: ${msg}`);
128
+ (0, uploader_js_1.writeStatusFile)('error', { error: msg });
129
+ throw new Error(msg);
130
+ }
120
131
  try {
121
132
  switch (framework) {
122
133
  case 'nestjs':
@@ -141,6 +152,20 @@ async function analyze(app, options) {
141
152
  }
142
153
  }
143
154
  break;
155
+ case 'fastify':
156
+ if (!app) {
157
+ const msg = 'Fastify app instance required — pass your app object to analyze()';
158
+ console.error(`❌ ${msg}`);
159
+ (0, uploader_js_1.writeStatusFile)('error', { error: msg });
160
+ return [];
161
+ }
162
+ routes = await (0, fastify_js_1.analyzeFastify)(app, {
163
+ scopeCatalogPath: options.scopeCatalogPath,
164
+ errorCatalogPath: options.errorCatalogPath,
165
+ envelopes: options.envelopes,
166
+ annotationsPath: options.annotationsPath,
167
+ });
168
+ break;
144
169
  case 'nextjs-pages':
145
170
  routes = await (0, nextjs_pages_js_1.analyzeNextPages)(options.nextDir || 'pages/api');
146
171
  break;
@@ -153,11 +178,8 @@ async function analyze(app, options) {
153
178
  case 'react-router':
154
179
  routes = await (0, react_router_js_1.analyzeReactRouter)(options.nextDir || 'src/routes.tsx');
155
180
  break;
156
- default:
157
- const msg = `Framework "${framework}" not supported yet (express, nextjs-pages, nextjs-app, vite, react-router)`;
158
- console.warn(`⚠️ APIForge: ${msg}`);
159
- (0, uploader_js_1.writeStatusFile)('error', { error: msg });
160
- return [];
181
+ // No default: unsupported frameworks are rejected above, before this
182
+ // try block, so a thrown error here isn't silently swallowed into `[]`.
161
183
  }
162
184
  }
163
185
  catch (err) {
@@ -213,6 +235,7 @@ exports.default = {
213
235
  analyzeExpress: express_js_1.analyzeExpress,
214
236
  analyzeNextPages: nextjs_pages_js_1.analyzeNextPages,
215
237
  analyzeNextApp: nextjs_app_js_1.analyzeNextApp,
238
+ analyzeFastify: fastify_js_1.analyzeFastify,
216
239
  analyzeVite: vite_js_1.analyzeVite,
217
240
  analyzeReactRouter: react_router_js_1.analyzeReactRouter,
218
241
  analyzeNestJS: nestjs_js_1.analyzeNestJS,
@@ -1 +1 @@
1
- {"type":"commonjs"}
1
+ {"type":"commonjs"}
package/dist/esm/cli.js CHANGED
@@ -219,11 +219,18 @@ async function main() {
219
219
  if (serverResult.status === 'error') {
220
220
  s.stop(red(`✗ ${serverResult.message}`));
221
221
  // Give a targeted hint for common failure modes
222
- if (serverResult.message.includes('API key')) {
223
- log.warn(yellow('Hint: The CLI wrote your key to .env.local — but your server may not load it.'));
224
- log.warn(yellow(' Add this at the top of your entry file before calling analyze():'));
225
- log.info(` ${cyan("require('dotenv').config({ path: '.env.local' })")}`);
226
- log.warn(yellow(' Or install dotenv: npm install dotenv'));
222
+ if (serverResult.message.includes('API key') || serverResult.message.includes('Invalid API key')) {
223
+ log.warn(yellow('Hint: Your API key was rejected by the server. Common causes:'));
224
+ log.warn(yellow(''));
225
+ log.warn(yellow(' 1. Key not yet created — go to your dashboard and generate one:'));
226
+ log.info(` ${cyan('https://apiforge.brainfogagency.com/dashboard')}`);
227
+ log.warn(yellow(''));
228
+ log.warn(yellow(' 2. Key belongs to a different account — make sure you\'re logged in'));
229
+ log.warn(yellow(' to the same account that owns this project.'));
230
+ log.warn(yellow(''));
231
+ log.warn(yellow(' 3. Server not loading .env.local — add this at the top of your entry file:'));
232
+ log.info(` ${cyan("require('dotenv').config({ path: '.env.local' })")}`);
233
+ log.warn(yellow(' Or install dotenv: npm install dotenv'));
227
234
  }
228
235
  else if (serverResult.message.includes('No routes')) {
229
236
  log.warn(yellow('Hint: Make sure all routes are registered before analyze() is called.'));
@@ -233,6 +240,14 @@ async function main() {
233
240
  log.warn(yellow('Hint: Your server might be taking too long to start, or the analyzer'));
234
241
  log.warn(yellow(' is not being called. Check your server logs for APIForge messages.'));
235
242
  }
243
+ // Show captured server output to help debug the root cause
244
+ const capturedOutput = serverResult.serverOutput;
245
+ if (capturedOutput) {
246
+ log.info('');
247
+ log.info(dim('─── Server output ───────────────────────────────'));
248
+ capturedOutput.split('\n').slice(0, 25).forEach((line) => log.info(dim(` ${line}`)));
249
+ log.info(dim('─────────────────────────────────────────────────'));
250
+ }
236
251
  await offerCleanup(entryFile, injected);
237
252
  outro(red('Analysis failed.'));
238
253
  process.exit(1);
@@ -744,11 +759,20 @@ function startAndWaitForAnalysis(command, timeoutMs) {
744
759
  const statusPath = join(process.cwd(), STATUS_FILE);
745
760
  const child = spawn(command, {
746
761
  cwd: process.cwd(),
747
- stdio: 'ignore',
762
+ stdio: ['ignore', 'pipe', 'pipe'],
748
763
  shell: true,
749
764
  env: { ...process.env },
750
765
  detached: false,
751
766
  });
767
+ // Capture server output (head-capped) so we can show it on failure
768
+ let serverOutput = '';
769
+ const MAX_OUTPUT = 4000;
770
+ const captureOutput = (chunk) => {
771
+ if (serverOutput.length < MAX_OUTPUT)
772
+ serverOutput += chunk.toString();
773
+ };
774
+ child.stdout?.on('data', captureOutput);
775
+ child.stderr?.on('data', captureOutput);
752
776
  let resolved = false;
753
777
  function finish(result) {
754
778
  if (resolved)
@@ -760,7 +784,7 @@ function startAndWaitForAnalysis(command, timeoutMs) {
760
784
  child.kill('SIGTERM');
761
785
  }
762
786
  catch { }
763
- resolve(result);
787
+ resolve({ ...result, serverOutput: serverOutput.trim() });
764
788
  }
765
789
  // Poll for the status file every 500 ms
766
790
  const poller = setInterval(() => {
@@ -1244,6 +1268,12 @@ function detectAppVariable(content) {
1244
1268
  /(?:const|let|var)\s+(\w+)\s*=\s*(?:fastify|Fastify)\s*\(/,
1245
1269
  /(?:const|let|var)\s+(\w+)\s*=\s*new\s+Koa\s*\(/,
1246
1270
  /(?:const|let|var)\s+(\w+)\s*=\s*(?:Router|createApp|createServer)\s*\(/,
1271
+ // Inline `require('fastify')(...)` / `require('express')(...)` — the app
1272
+ // factory is never bound to a local name, only its return value is. This
1273
+ // is at least as common as the named-import style above (it's the pattern
1274
+ // in Fastify's own project generator output), and the patterns above never
1275
+ // match it because there's no `fastify`/`express` identifier in the source.
1276
+ /(?:const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*['"](?:fastify|express)['"]\s*\)\s*(?:\.default\s*)?\(/,
1247
1277
  ];
1248
1278
  for (const re of patterns) {
1249
1279
  const m = content.match(re);
@@ -1,5 +1,19 @@
1
1
  import { existsSync } from 'fs';
2
2
  import { join } from 'path';
3
+ /**
4
+ * Frameworks analyze() can actually dispatch to. Detection can identify more
5
+ * frameworks than this (koa, hapi) — those are named so the detector is honest
6
+ * about what it saw, but `supported: false` tells callers not to expect output.
7
+ */
8
+ export const SUPPORTED_FRAMEWORKS = new Set([
9
+ 'express',
10
+ 'fastify',
11
+ 'nestjs',
12
+ 'nextjs-pages',
13
+ 'nextjs-app',
14
+ 'vite',
15
+ 'react-router',
16
+ ]);
3
17
  const cwd = () => process.cwd();
4
18
  function fileExists(rel) {
5
19
  return existsSync(join(cwd(), rel));
@@ -18,16 +32,19 @@ function tryResolve(mod) {
18
32
  * Detect the web framework from the live app instance + filesystem hints.
19
33
  * Confidence is used to pick the best match when multiple signals fire.
20
34
  */
35
+ function mk(name, confidence) {
36
+ return { name, confidence, supported: SUPPORTED_FRAMEWORKS.has(name) };
37
+ }
21
38
  export function detectFramework(app) {
22
39
  // ── Instance-based detection (highest confidence) ─────────────────────────
23
40
  if (app && typeof app === 'function') {
24
41
  // Express: _router or lazyrouter
25
42
  if (app._router || (app.lazyrouter && typeof app.lazyrouter === 'function')) {
26
- return { name: 'express', confidence: 1.0 };
43
+ return mk('express', 1.0);
27
44
  }
28
45
  // Koa: middleware array
29
46
  if (Array.isArray(app.middleware) && typeof app.use === 'function' && app.context) {
30
- return { name: 'koa', confidence: 0.95 };
47
+ return mk('koa', 0.95);
31
48
  }
32
49
  }
33
50
  // Fastify: inject method and route method
@@ -35,7 +52,7 @@ export function detectFramework(app) {
35
52
  typeof app === 'object' &&
36
53
  typeof app.inject === 'function' &&
37
54
  typeof app.route === 'function') {
38
- return { name: 'fastify', confidence: 1.0 };
55
+ return mk('fastify', 1.0);
39
56
  }
40
57
  // Hapi: server with route + start methods
41
58
  if (app &&
@@ -43,43 +60,43 @@ export function detectFramework(app) {
43
60
  typeof app.route === 'function' &&
44
61
  typeof app.start === 'function' &&
45
62
  typeof app.stop === 'function') {
46
- return { name: 'hapi', confidence: 0.95 };
63
+ return mk('hapi', 0.95);
47
64
  }
48
65
  // NestJS application
49
66
  if (app &&
50
67
  typeof app === 'object' &&
51
68
  typeof app.getHttpServer === 'function' &&
52
69
  typeof app.listen === 'function') {
53
- return { name: 'nestjs', confidence: 0.95 };
70
+ return mk('nestjs', 0.95);
54
71
  }
55
72
  // ── Filesystem detection ──────────────────────────────────────────────────
56
73
  // Next.js App Router
57
74
  if (fileExists('app') && (fileExists('app/layout.tsx') || fileExists('app/layout.js'))) {
58
- return { name: 'nextjs-app', confidence: 0.92 };
75
+ return mk('nextjs-app', 0.92);
59
76
  }
60
77
  // Next.js Pages Router
61
78
  if (fileExists('pages/api') || fileExists('src/pages/api')) {
62
- return { name: 'nextjs-pages', confidence: 0.90 };
79
+ return mk('nextjs-pages', 0.90);
63
80
  }
64
81
  if (fileExists('next.config.js') ||
65
82
  fileExists('next.config.mjs') ||
66
83
  fileExists('next.config.ts')) {
67
- return { name: 'nextjs-pages', confidence: 0.75 };
84
+ return mk('nextjs-pages', 0.75);
68
85
  }
69
86
  // ── Dependency-based detection (lowest confidence) ────────────────────────
70
87
  if (tryResolve('express'))
71
- return { name: 'express', confidence: 0.70 };
88
+ return mk('express', 0.70);
72
89
  if (tryResolve('fastify'))
73
- return { name: 'fastify', confidence: 0.70 };
90
+ return mk('fastify', 0.70);
74
91
  if (tryResolve('koa'))
75
- return { name: 'koa', confidence: 0.70 };
92
+ return mk('koa', 0.70);
76
93
  if (tryResolve('@hapi/hapi') || tryResolve('hapi'))
77
- return { name: 'hapi', confidence: 0.70 };
94
+ return mk('hapi', 0.70);
78
95
  if (tryResolve('@nestjs/core'))
79
- return { name: 'nestjs', confidence: 0.70 };
96
+ return mk('nestjs', 0.70);
80
97
  if (tryResolve('vite'))
81
- return { name: 'vite', confidence: 0.60 };
98
+ return mk('vite', 0.60);
82
99
  if (tryResolve('react-router-dom'))
83
- return { name: 'react-router', confidence: 0.55 };
84
- return { name: 'unknown', confidence: 0 };
100
+ return mk('react-router', 0.55);
101
+ return mk('unknown', 0);
85
102
  }