apiforge-analyzer 1.3.1 → 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/README.md +165 -3
- package/dist/cjs/cli.js +236 -21
- package/dist/cjs/detect.js +34 -16
- package/dist/cjs/frameworks/fastify.js +1112 -0
- package/dist/cjs/index.js +29 -6
- package/dist/cjs/package.json +1 -1
- package/dist/esm/cli.js +237 -22
- package/dist/esm/detect.js +33 -16
- package/dist/esm/frameworks/fastify.js +1076 -0
- package/dist/esm/index.js +29 -7
- package/dist/types/detect.d.ts +7 -3
- package/dist/types/frameworks/fastify.d.ts +16 -0
- package/dist/types/index.d.ts +16 -1
- package/package.json +7 -4
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
|
-
|
|
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,
|
package/dist/cjs/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"type":"commonjs"}
|
|
1
|
+
{"type":"commonjs"}
|
package/dist/esm/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { group, intro, outro, text, select, password, confirm, tasks, note, log, isCancel, cancel, spinner, } from '@clack/prompts';
|
|
3
3
|
import { readFileSync, existsSync, writeFileSync, unlinkSync, readdirSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
4
|
+
import { join, basename } from 'path';
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
6
|
import axios from 'axios';
|
|
7
7
|
import { detectArchitectureType } from './arch-detect.js';
|
|
@@ -22,7 +22,7 @@ const green = (s) => `${gn}${s}${r}`;
|
|
|
22
22
|
const yellow = (s) => `${yl}${s}${r}`;
|
|
23
23
|
const red = (s) => `${rd}${s}${r}`;
|
|
24
24
|
const white = (s) => `${wh}${s}${r}`;
|
|
25
|
-
const VERSION = '1.3.
|
|
25
|
+
const VERSION = '1.3.2';
|
|
26
26
|
// Built-in URLs — users never need to configure these
|
|
27
27
|
const BACKEND_PROD = 'https://apiforgeapi.brainfogagency.com';
|
|
28
28
|
const DASHBOARD_PROD = 'https://apiforge.brainfogagency.com/dashboard';
|
|
@@ -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:
|
|
224
|
-
log.warn(yellow('
|
|
225
|
-
log.
|
|
226
|
-
log.
|
|
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);
|
|
@@ -390,6 +405,158 @@ function hasDockerCompose(cwd) {
|
|
|
390
405
|
return ['docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml']
|
|
391
406
|
.some(f => existsSync(join(cwd, f)));
|
|
392
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Find the gateway among the discovered services (by name or by proxy-middleware
|
|
410
|
+
* usage) and parse its prefix → service map, e.g. { auth: '/auth', gold_rates: '/gold-rates' }.
|
|
411
|
+
* Matches the common `'/prefix': process.env.X_SERVICE_URL || ...` gateway route-table
|
|
412
|
+
* pattern (express-gateway / http-proxy-middleware style). Returns {} if no gateway
|
|
413
|
+
* or no recognizable mapping is found — callers should treat that as "no rewrite."
|
|
414
|
+
*/
|
|
415
|
+
function detectGatewayPrefixMap(cwd, services) {
|
|
416
|
+
const prefixMap = {};
|
|
417
|
+
for (const svc of services) {
|
|
418
|
+
const svcDir = svc.dir.replace(/^\.\//, '');
|
|
419
|
+
const relEntry = svc.entryFile ?? findEntryInDir(join(cwd, svcDir));
|
|
420
|
+
if (!relEntry)
|
|
421
|
+
continue;
|
|
422
|
+
let source = '';
|
|
423
|
+
try {
|
|
424
|
+
source = readFileSync(join(cwd, svcDir, relEntry), 'utf-8');
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const looksLikeGateway = /gateway/i.test(svc.name) || /createProxyMiddleware|http-proxy-middleware/.test(source);
|
|
430
|
+
if (!looksLikeGateway)
|
|
431
|
+
continue;
|
|
432
|
+
// '/auth': process.env.AUTH_SERVICE_URL || `http://...`
|
|
433
|
+
const re = /['"](\/[a-zA-Z0-9_-]*)['"]\s*:\s*process\.env\.([A-Z][A-Z0-9_]*)/g;
|
|
434
|
+
let m;
|
|
435
|
+
while ((m = re.exec(source))) {
|
|
436
|
+
const prefix = m[1];
|
|
437
|
+
const serviceName = m[2].replace(/_SERVICE_URL$|_URL$/, '').toLowerCase();
|
|
438
|
+
if (!prefixMap[serviceName])
|
|
439
|
+
prefixMap[serviceName] = prefix; // first (primary) mount wins over aliases
|
|
440
|
+
}
|
|
441
|
+
if (Object.keys(prefixMap).length > 0)
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
return prefixMap;
|
|
445
|
+
}
|
|
446
|
+
function withGatewayPrefix(prefix, routePath) {
|
|
447
|
+
if (routePath === '/')
|
|
448
|
+
return prefix;
|
|
449
|
+
return prefix.replace(/\/$/, '') + (routePath.startsWith('/') ? routePath : `/${routePath}`);
|
|
450
|
+
}
|
|
451
|
+
// ─── Postman collection enrichment ─────────────────────────────────────────────
|
|
452
|
+
// Runtime introspection only sees field NAMES (via regex over handler source),
|
|
453
|
+
// never real example values — a Postman collection, when present, already has
|
|
454
|
+
// hand-written example bodies and descriptions. We fold those in as enrichment
|
|
455
|
+
// only: matched by method + path, never adds or removes an endpoint.
|
|
456
|
+
function findFileRecursive(dir, matches, maxDepth, depth = 0) {
|
|
457
|
+
if (depth > maxDepth)
|
|
458
|
+
return null;
|
|
459
|
+
let entries;
|
|
460
|
+
try {
|
|
461
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
for (const e of entries) {
|
|
467
|
+
if (e.isFile() && matches(e.name))
|
|
468
|
+
return join(dir, e.name);
|
|
469
|
+
}
|
|
470
|
+
for (const e of entries) {
|
|
471
|
+
if (e.isDirectory() && !['node_modules', '.git', 'dist', 'build', '.next'].includes(e.name)) {
|
|
472
|
+
const found = findFileRecursive(join(dir, e.name), matches, maxDepth, depth + 1);
|
|
473
|
+
if (found)
|
|
474
|
+
return found;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
function findPostmanCollection(cwd) {
|
|
480
|
+
const isCollection = (name) => name.endsWith('.postman_collection.json');
|
|
481
|
+
for (const dir of ['postman', 'docs/postman', '.postman', 'collections']) {
|
|
482
|
+
const dirPath = join(cwd, dir);
|
|
483
|
+
if (!existsSync(dirPath))
|
|
484
|
+
continue;
|
|
485
|
+
try {
|
|
486
|
+
const match = readdirSync(dirPath).find(isCollection);
|
|
487
|
+
if (match)
|
|
488
|
+
return join(dirPath, match);
|
|
489
|
+
}
|
|
490
|
+
catch { }
|
|
491
|
+
}
|
|
492
|
+
return findFileRecursive(cwd, isCollection, 3);
|
|
493
|
+
}
|
|
494
|
+
/** "{{baseUrl}}/gold-rates/graph/{{userId}}?interval=x" -> "/gold-rates/graph/:userId" */
|
|
495
|
+
function postmanRawToRoutePath(raw) {
|
|
496
|
+
let p = raw.trim();
|
|
497
|
+
p = p.replace(/^\{\{[^}]+\}\}/, '');
|
|
498
|
+
p = p.split('?')[0];
|
|
499
|
+
p = p.replace(/\{\{([a-zA-Z0-9_]+)\}\}/g, ':$1');
|
|
500
|
+
if (!p)
|
|
501
|
+
p = '/';
|
|
502
|
+
if (!p.startsWith('/'))
|
|
503
|
+
p = '/' + p;
|
|
504
|
+
if (p !== '/' && p.endsWith('/'))
|
|
505
|
+
p = p.slice(0, -1);
|
|
506
|
+
return p;
|
|
507
|
+
}
|
|
508
|
+
/** Recursively walk a Postman collection's item tree (folders nest via `item`; requests carry `request`). */
|
|
509
|
+
function walkPostmanItems(items, index) {
|
|
510
|
+
for (const node of items ?? []) {
|
|
511
|
+
if (Array.isArray(node.item))
|
|
512
|
+
walkPostmanItems(node.item, index);
|
|
513
|
+
if (!node.request)
|
|
514
|
+
continue;
|
|
515
|
+
const req = node.request;
|
|
516
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
517
|
+
const rawUrl = typeof req.url === 'string' ? req.url : req.url?.raw;
|
|
518
|
+
if (!rawUrl)
|
|
519
|
+
continue;
|
|
520
|
+
const key = `${method} ${postmanRawToRoutePath(rawUrl)}`;
|
|
521
|
+
if (index.has(key))
|
|
522
|
+
continue; // first match wins — collection often repeats the same endpoint in a smoke-test folder
|
|
523
|
+
const entry = {};
|
|
524
|
+
if (req.body?.mode === 'raw' && typeof req.body.raw === 'string' && req.body.raw.trim()) {
|
|
525
|
+
entry.bodyExample = req.body.raw.trim();
|
|
526
|
+
}
|
|
527
|
+
if (typeof req.description === 'string' && req.description.trim()) {
|
|
528
|
+
entry.description = req.description.trim();
|
|
529
|
+
}
|
|
530
|
+
if (entry.bodyExample || entry.description)
|
|
531
|
+
index.set(key, entry);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/** Enrich collected routes in-place with Postman example bodies/descriptions, matched by method+path. Never adds or drops routes. */
|
|
535
|
+
function enrichRoutesFromPostman(cwd, routes) {
|
|
536
|
+
const collectionPath = findPostmanCollection(cwd);
|
|
537
|
+
if (!collectionPath)
|
|
538
|
+
return;
|
|
539
|
+
try {
|
|
540
|
+
const collection = JSON.parse(readFileSync(collectionPath, 'utf-8'));
|
|
541
|
+
const index = new Map();
|
|
542
|
+
walkPostmanItems(collection.item, index);
|
|
543
|
+
let enriched = 0;
|
|
544
|
+
for (const route of routes) {
|
|
545
|
+
const match = index.get(`${route.method} ${route.path}`);
|
|
546
|
+
if (!match)
|
|
547
|
+
continue;
|
|
548
|
+
if (match.bodyExample)
|
|
549
|
+
route.bodyExample = match.bodyExample;
|
|
550
|
+
if (match.description && !route.description)
|
|
551
|
+
route.description = match.description;
|
|
552
|
+
enriched++;
|
|
553
|
+
}
|
|
554
|
+
if (enriched > 0) {
|
|
555
|
+
log.info(dim(`Enriched ${enriched} routes with examples from ${basename(collectionPath)}`));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
catch { }
|
|
559
|
+
}
|
|
393
560
|
async function microservicesFlow(config, opts) {
|
|
394
561
|
const cwd = process.cwd();
|
|
395
562
|
const allRoutes = [];
|
|
@@ -432,6 +599,14 @@ async function microservicesFlow(config, opts) {
|
|
|
432
599
|
'',
|
|
433
600
|
dim('Each service is started individually and its routes are collected.'),
|
|
434
601
|
].join('\n'), 'Multi-service analysis');
|
|
602
|
+
// Routes are collected at each service's own internal paths (e.g. auth's /register),
|
|
603
|
+
// not the path clients actually call through the gateway (/auth/register) — Express
|
|
604
|
+
// strips the mount prefix before the service ever sees the request. Rewrite paths
|
|
605
|
+
// here so the docs match what's actually callable.
|
|
606
|
+
const gatewayPrefixes = detectGatewayPrefixMap(cwd, config.services);
|
|
607
|
+
if (Object.keys(gatewayPrefixes).length > 0) {
|
|
608
|
+
log.info(dim(`Gateway prefixes detected: ${Object.entries(gatewayPrefixes).map(([s, p]) => `${s}→${p}`).join(', ')}`));
|
|
609
|
+
}
|
|
435
610
|
for (const svc of config.services) {
|
|
436
611
|
const svcSpin = spinner();
|
|
437
612
|
svcSpin.start(`${bold(svc.name)} — starting…`);
|
|
@@ -460,13 +635,14 @@ async function microservicesFlow(config, opts) {
|
|
|
460
635
|
detached: false,
|
|
461
636
|
});
|
|
462
637
|
// Capture output so failures are debuggable without re-running the service by hand.
|
|
463
|
-
//
|
|
638
|
+
// Keep the HEAD, not the tail: Node prints "Error: ..." / "Cannot find module ..."
|
|
639
|
+
// first, then unwinds the call stack — a long stack trace must not push that
|
|
640
|
+
// opening line out of the window, so stop growing once capped instead of sliding.
|
|
464
641
|
let output = '';
|
|
465
|
-
const MAX_OUTPUT =
|
|
642
|
+
const MAX_OUTPUT = 6000;
|
|
466
643
|
const capture = (chunk) => {
|
|
467
|
-
output
|
|
468
|
-
|
|
469
|
-
output = output.slice(-MAX_OUTPUT);
|
|
644
|
+
if (output.length < MAX_OUTPUT)
|
|
645
|
+
output += chunk.toString();
|
|
470
646
|
};
|
|
471
647
|
child.stdout?.on('data', capture);
|
|
472
648
|
child.stderr?.on('data', capture);
|
|
@@ -479,7 +655,12 @@ async function microservicesFlow(config, opts) {
|
|
|
479
655
|
catch { }
|
|
480
656
|
tryUnlink(join(cwd, statusFile));
|
|
481
657
|
if (routes && routes.length > 0) {
|
|
482
|
-
const
|
|
658
|
+
const prefix = gatewayPrefixes[svc.name];
|
|
659
|
+
const annotated = routes.map((r) => ({
|
|
660
|
+
...r,
|
|
661
|
+
service: svc.name,
|
|
662
|
+
path: prefix ? withGatewayPrefix(prefix, r.path) : r.path,
|
|
663
|
+
}));
|
|
483
664
|
allRoutes.push(...annotated);
|
|
484
665
|
serviceMeta.push({ name: svc.name, baseUrl: svc.baseUrl });
|
|
485
666
|
svcSpin.stop(green(`✓ ${svc.name} — ${routes.length} routes collected`));
|
|
@@ -488,7 +669,7 @@ async function microservicesFlow(config, opts) {
|
|
|
488
669
|
svcSpin.stop(yellow(`◌ ${svc.name} — no routes collected (timeout or error)`));
|
|
489
670
|
const trimmed = output.trim();
|
|
490
671
|
if (trimmed) {
|
|
491
|
-
log.error(dim(` ${bold(svc.name)} output:\n` + trimmed.split('\n').slice(
|
|
672
|
+
log.error(dim(` ${bold(svc.name)} output:\n` + trimmed.split('\n').slice(0, 30).map(l => ` ${l}`).join('\n')));
|
|
492
673
|
}
|
|
493
674
|
}
|
|
494
675
|
// Remove injected code
|
|
@@ -498,6 +679,7 @@ async function microservicesFlow(config, opts) {
|
|
|
498
679
|
// Safety: clean up any remaining injections (e.g. if process was interrupted)
|
|
499
680
|
for (const f of injectedFiles)
|
|
500
681
|
removeInjectedCode(f);
|
|
682
|
+
enrichRoutesFromPostman(cwd, allRoutes);
|
|
501
683
|
if (allRoutes.length === 0) {
|
|
502
684
|
log.error(red('No routes were collected from any service.'));
|
|
503
685
|
outro(red('Analysis failed.'));
|
|
@@ -577,11 +759,20 @@ function startAndWaitForAnalysis(command, timeoutMs) {
|
|
|
577
759
|
const statusPath = join(process.cwd(), STATUS_FILE);
|
|
578
760
|
const child = spawn(command, {
|
|
579
761
|
cwd: process.cwd(),
|
|
580
|
-
stdio: 'ignore',
|
|
762
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
581
763
|
shell: true,
|
|
582
764
|
env: { ...process.env },
|
|
583
765
|
detached: false,
|
|
584
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);
|
|
585
776
|
let resolved = false;
|
|
586
777
|
function finish(result) {
|
|
587
778
|
if (resolved)
|
|
@@ -593,7 +784,7 @@ function startAndWaitForAnalysis(command, timeoutMs) {
|
|
|
593
784
|
child.kill('SIGTERM');
|
|
594
785
|
}
|
|
595
786
|
catch { }
|
|
596
|
-
resolve(result);
|
|
787
|
+
resolve({ ...result, serverOutput: serverOutput.trim() });
|
|
597
788
|
}
|
|
598
789
|
// Poll for the status file every 500 ms
|
|
599
790
|
const poller = setInterval(() => {
|
|
@@ -1018,13 +1209,33 @@ function buildSnippet(projectName, apiKey, baseUrl, backendUrl, appVar, isESM, a
|
|
|
1018
1209
|
` upload: true,`,
|
|
1019
1210
|
` runOnce: true,`,
|
|
1020
1211
|
].join('\n');
|
|
1212
|
+
// Collection mode: some services only mount routes inside an async bootstrap
|
|
1213
|
+
// gated behind a DB/broker connect (which may hang/fail outside Docker).
|
|
1214
|
+
// Poll briefly for app._router to be populated (Express-shaped apps only)
|
|
1215
|
+
// before giving up and collecting whatever is there — bounded well under
|
|
1216
|
+
// the CLI's 30s per-service timeout so a hung connect still surfaces.
|
|
1217
|
+
const runAnalyze = `APIForge.analyze(${appVar}, {\n${opts}\n })`;
|
|
1218
|
+
const call = collectionOpts
|
|
1219
|
+
? [
|
|
1220
|
+
` let __apiforgeTries = 0`,
|
|
1221
|
+
` const __apiforgeTryCollect = () => {`,
|
|
1222
|
+
` const hasRoutes = !${appVar} || !${appVar}._router`,
|
|
1223
|
+
` ? true`,
|
|
1224
|
+
` : Array.isArray(${appVar}._router.stack) && ${appVar}._router.stack.length > 0`,
|
|
1225
|
+
` if (hasRoutes || __apiforgeTries++ >= 26) {`,
|
|
1226
|
+
` ${runAnalyze}`,
|
|
1227
|
+
` } else {`,
|
|
1228
|
+
` setTimeout(__apiforgeTryCollect, 300)`,
|
|
1229
|
+
` }`,
|
|
1230
|
+
` }`,
|
|
1231
|
+
` __apiforgeTryCollect()`,
|
|
1232
|
+
].join('\n')
|
|
1233
|
+
: ` ${runAnalyze}`;
|
|
1021
1234
|
if (isESM) {
|
|
1022
1235
|
return [
|
|
1023
1236
|
`// APIForge — https://apiforge.brainfogagency.com`,
|
|
1024
1237
|
`import('apiforge-analyzer').then(({ default: APIForge }) => {`,
|
|
1025
|
-
|
|
1026
|
-
opts,
|
|
1027
|
-
` })`,
|
|
1238
|
+
call,
|
|
1028
1239
|
`}).catch(() => {})`,
|
|
1029
1240
|
'',
|
|
1030
1241
|
].join('\n');
|
|
@@ -1033,9 +1244,7 @@ function buildSnippet(projectName, apiKey, baseUrl, backendUrl, appVar, isESM, a
|
|
|
1033
1244
|
`// APIForge — https://apiforge.brainfogagency.com`,
|
|
1034
1245
|
`Promise.resolve().then(() => require('apiforge-analyzer')).then(mod => {`,
|
|
1035
1246
|
` const APIForge = mod.default || mod`,
|
|
1036
|
-
|
|
1037
|
-
opts,
|
|
1038
|
-
` })`,
|
|
1247
|
+
call,
|
|
1039
1248
|
`}).catch(() => {})`,
|
|
1040
1249
|
'',
|
|
1041
1250
|
].join('\n');
|
|
@@ -1059,6 +1268,12 @@ function detectAppVariable(content) {
|
|
|
1059
1268
|
/(?:const|let|var)\s+(\w+)\s*=\s*(?:fastify|Fastify)\s*\(/,
|
|
1060
1269
|
/(?:const|let|var)\s+(\w+)\s*=\s*new\s+Koa\s*\(/,
|
|
1061
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*)?\(/,
|
|
1062
1277
|
];
|
|
1063
1278
|
for (const re of patterns) {
|
|
1064
1279
|
const m = content.match(re);
|
package/dist/esm/detect.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
84
|
+
return mk('nextjs-pages', 0.75);
|
|
68
85
|
}
|
|
69
86
|
// ── Dependency-based detection (lowest confidence) ────────────────────────
|
|
70
87
|
if (tryResolve('express'))
|
|
71
|
-
return
|
|
88
|
+
return mk('express', 0.70);
|
|
72
89
|
if (tryResolve('fastify'))
|
|
73
|
-
return
|
|
90
|
+
return mk('fastify', 0.70);
|
|
74
91
|
if (tryResolve('koa'))
|
|
75
|
-
return
|
|
92
|
+
return mk('koa', 0.70);
|
|
76
93
|
if (tryResolve('@hapi/hapi') || tryResolve('hapi'))
|
|
77
|
-
return
|
|
94
|
+
return mk('hapi', 0.70);
|
|
78
95
|
if (tryResolve('@nestjs/core'))
|
|
79
|
-
return
|
|
96
|
+
return mk('nestjs', 0.70);
|
|
80
97
|
if (tryResolve('vite'))
|
|
81
|
-
return
|
|
98
|
+
return mk('vite', 0.60);
|
|
82
99
|
if (tryResolve('react-router-dom'))
|
|
83
|
-
return
|
|
84
|
-
return
|
|
100
|
+
return mk('react-router', 0.55);
|
|
101
|
+
return mk('unknown', 0);
|
|
85
102
|
}
|