apiforge-analyzer 1.1.0 → 1.3.0
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 +166 -197
- package/dist/cjs/arch-detect.js +133 -0
- package/dist/cjs/cli.js +866 -244
- package/dist/cjs/detect.js +68 -48
- package/dist/cjs/export.js +2 -0
- package/dist/cjs/frameworks/express.js +80 -74
- package/dist/cjs/frameworks/nestjs.js +38 -0
- package/dist/cjs/index.js +158 -60
- package/dist/cjs/modules/express.js +65 -0
- package/dist/cjs/modules/nestjs.js +50 -0
- package/dist/cjs/uploader.js +51 -11
- package/dist/esm/arch-detect.js +130 -0
- package/dist/esm/cli.js +865 -246
- package/dist/esm/detect.js +68 -48
- package/dist/esm/export.js +2 -0
- package/dist/esm/frameworks/express.js +80 -74
- package/dist/esm/frameworks/nestjs.js +35 -0
- package/dist/esm/index.js +158 -62
- package/dist/esm/modules/express.js +60 -0
- package/dist/esm/modules/nestjs.js +47 -0
- package/dist/esm/uploader.js +49 -11
- package/dist/types/arch-detect.d.ts +9 -0
- package/dist/types/detect.d.ts +3 -5
- package/dist/types/export.d.ts +2 -0
- package/dist/types/frameworks/express.d.ts +2 -2
- package/dist/types/frameworks/nestjs.d.ts +10 -0
- package/dist/types/index.d.ts +12 -5
- package/dist/types/modules/express.d.ts +22 -0
- package/dist/types/modules/nestjs.d.ts +6 -0
- package/dist/types/uploader.d.ts +17 -2
- package/package.json +3 -2
package/dist/esm/index.js
CHANGED
|
@@ -1,107 +1,201 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* APIForge Analyzer — main entry point (dual ESM/CJS)
|
|
3
3
|
*/
|
|
4
|
+
import { writeFileSync, readFileSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
4
6
|
import { analyzeExpress } from './frameworks/express.js';
|
|
5
7
|
import { analyzeNextPages } from './frameworks/nextjs-pages.js';
|
|
6
8
|
import { analyzeNextApp } from './frameworks/nextjs-app.js';
|
|
7
9
|
import { analyzeVite } from './frameworks/vite.js';
|
|
8
10
|
import { analyzeReactRouter } from './frameworks/react-router.js';
|
|
11
|
+
import { analyzeNestJS } from './frameworks/nestjs.js';
|
|
9
12
|
import { detectFramework } from './detect.js';
|
|
10
13
|
import { exportToJSON } from './export.js';
|
|
11
|
-
import { uploadRoutes } from './uploader.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
import { uploadRoutes, writeStatusFile } from './uploader.js';
|
|
15
|
+
import { detectArchitectureType } from './arch-detect.js';
|
|
16
|
+
import { detectExpressModules, annotateRoutesWithModules } from './modules/express.js';
|
|
17
|
+
import { detectNestModules } from './modules/nestjs.js';
|
|
18
|
+
// ─── Env file loader — no dotenv dependency ───────────────────────────────────
|
|
15
19
|
/**
|
|
16
|
-
*
|
|
20
|
+
* Read a single variable from a .env-style file.
|
|
21
|
+
* Plain Node/Express doesn't auto-load .env.local, so we do it ourselves.
|
|
22
|
+
* The package never requires dotenv or any other dep for this.
|
|
17
23
|
*/
|
|
24
|
+
function readEnvFile(varName) {
|
|
25
|
+
const candidates = [
|
|
26
|
+
'.env.local',
|
|
27
|
+
'.env.development.local',
|
|
28
|
+
'.env.development',
|
|
29
|
+
'.env',
|
|
30
|
+
];
|
|
31
|
+
for (const file of candidates) {
|
|
32
|
+
try {
|
|
33
|
+
const content = readFileSync(join(process.cwd(), file), 'utf-8');
|
|
34
|
+
for (const line of content.split('\n')) {
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
if (trimmed.startsWith('#') || !trimmed.includes('='))
|
|
37
|
+
continue;
|
|
38
|
+
const eqIdx = trimmed.indexOf('=');
|
|
39
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
40
|
+
if (key !== varName)
|
|
41
|
+
continue;
|
|
42
|
+
return trimmed.slice(eqIdx + 1).trim().replace(/^['"]|['"]$/g, '');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
// ─── File-based debounce (survives nodemon restarts) ──────────────────────────
|
|
50
|
+
const DEBOUNCE_FILE = '.apiforge-last-run.json';
|
|
51
|
+
const DEBOUNCE_MS = 30000; // 30 s
|
|
52
|
+
function readLastRunMs() {
|
|
53
|
+
try {
|
|
54
|
+
const raw = readFileSync(join(process.cwd(), DEBOUNCE_FILE), 'utf-8');
|
|
55
|
+
return JSON.parse(raw).timestamp ?? 0;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function stampLastRun() {
|
|
62
|
+
try {
|
|
63
|
+
writeFileSync(join(process.cwd(), DEBOUNCE_FILE), JSON.stringify({ timestamp: Date.now() }), 'utf-8');
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
}
|
|
67
|
+
// ─── Main analyze ─────────────────────────────────────────────────────────────
|
|
18
68
|
export async function analyze(app, options) {
|
|
19
|
-
//
|
|
69
|
+
// Never run in production
|
|
20
70
|
if (options.skipInProduction !== false && process.env.NODE_ENV === 'production') {
|
|
21
|
-
console.log('⏭️ Skipping analysis in production');
|
|
22
71
|
return [];
|
|
23
72
|
}
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
73
|
+
// File-based debounce — survives nodemon restarts.
|
|
74
|
+
// Skipped in collection mode (statusFile set) because the CLI controls each service start
|
|
75
|
+
// and the shared .apiforge-last-run.json would cause services 2+ to be skipped.
|
|
76
|
+
if (options.runOnce !== false && !options.statusFile) {
|
|
77
|
+
const elapsed = Date.now() - readLastRunMs();
|
|
78
|
+
if (elapsed < DEBOUNCE_MS) {
|
|
79
|
+
console.log(`⚡ APIForge: skipped (last run ${Math.round(elapsed / 1000)}s ago)`);
|
|
80
|
+
return [];
|
|
31
81
|
}
|
|
82
|
+
stampLastRun();
|
|
32
83
|
}
|
|
33
|
-
|
|
34
|
-
//
|
|
84
|
+
// ── Resolve API key ─────────────────────────────────────────────────────────
|
|
85
|
+
// Priority: explicit option → process.env → .env.local/.env file
|
|
86
|
+
// This means the package works even if the user's server doesn't load dotenv.
|
|
87
|
+
if (!options.apiKey) {
|
|
88
|
+
options.apiKey =
|
|
89
|
+
process.env.APIFORGE_API_KEY ||
|
|
90
|
+
readEnvFile('APIFORGE_API_KEY');
|
|
91
|
+
}
|
|
92
|
+
console.log(`🔍 APIForge: analyzing ${options.projectName}…`);
|
|
93
|
+
// Writing saveJson files into the watched dir triggers nodemon restart loops
|
|
35
94
|
if (options.upload !== false && options.apiKey) {
|
|
36
95
|
options.saveJson = false;
|
|
37
96
|
}
|
|
97
|
+
// ── Early failure: upload desired but no API key anywhere ──────────────────
|
|
98
|
+
if (options.upload !== false && !options.apiKey) {
|
|
99
|
+
const msg = 'APIFORGE_API_KEY not found in options, process.env, or .env.local';
|
|
100
|
+
console.error(`❌ APIForge: ${msg}`);
|
|
101
|
+
writeStatusFile('error', { error: msg });
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
38
104
|
let routes = [];
|
|
39
105
|
let framework = options.framework;
|
|
40
|
-
// Auto-detect framework if not specified
|
|
41
106
|
if (!framework) {
|
|
42
|
-
|
|
43
|
-
framework = detected.name;
|
|
107
|
+
framework = detectFramework(app).name;
|
|
44
108
|
console.log(`📦 Detected framework: ${framework}`);
|
|
45
109
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
110
|
+
try {
|
|
111
|
+
switch (framework) {
|
|
112
|
+
case 'nestjs':
|
|
113
|
+
routes = await analyzeNestJS(app, options.nextDir || process.cwd());
|
|
114
|
+
break;
|
|
115
|
+
case 'express':
|
|
116
|
+
if (!app) {
|
|
117
|
+
const msg = 'Express app instance required — pass your app object to analyze()';
|
|
118
|
+
console.error(`❌ ${msg}`);
|
|
119
|
+
writeStatusFile('error', { error: msg });
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
routes = await analyzeExpress(app);
|
|
123
|
+
// For modular-monolithic: annotate each route with its module name
|
|
124
|
+
if (options.architectureType === 'modular-monolithic') {
|
|
125
|
+
const dir = options.nextDir || process.cwd();
|
|
126
|
+
const modules = detectNestModules(dir).length > 0
|
|
127
|
+
? detectNestModules(dir)
|
|
128
|
+
: detectExpressModules(dir);
|
|
129
|
+
if (modules.length > 0) {
|
|
130
|
+
routes = annotateRoutesWithModules(routes, modules);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
case 'nextjs-pages':
|
|
135
|
+
routes = await analyzeNextPages(options.nextDir || 'pages/api');
|
|
136
|
+
break;
|
|
137
|
+
case 'nextjs-app':
|
|
138
|
+
routes = await analyzeNextApp(options.nextDir || 'app');
|
|
139
|
+
break;
|
|
140
|
+
case 'vite':
|
|
141
|
+
routes = await analyzeVite(options.nextDir || 'vite.config.js');
|
|
142
|
+
break;
|
|
143
|
+
case 'react-router':
|
|
144
|
+
routes = await analyzeReactRouter(options.nextDir || 'src/routes.tsx');
|
|
145
|
+
break;
|
|
146
|
+
default:
|
|
147
|
+
const msg = `Framework "${framework}" not supported yet (express, nextjs-pages, nextjs-app, vite, react-router)`;
|
|
148
|
+
console.warn(`⚠️ APIForge: ${msg}`);
|
|
149
|
+
writeStatusFile('error', { error: msg });
|
|
51
150
|
return [];
|
|
52
|
-
|
|
53
|
-
routes = await analyzeExpress(app);
|
|
54
|
-
break;
|
|
55
|
-
case 'nextjs-pages':
|
|
56
|
-
routes = await analyzeNextPages(options.nextDir || 'pages/api');
|
|
57
|
-
break;
|
|
58
|
-
case 'nextjs-app':
|
|
59
|
-
routes = await analyzeNextApp(options.nextDir || 'app');
|
|
60
|
-
break;
|
|
61
|
-
case 'vite':
|
|
62
|
-
routes = await analyzeVite(options.nextDir || 'vite.config.js');
|
|
63
|
-
break;
|
|
64
|
-
case 'react-router':
|
|
65
|
-
routes = await analyzeReactRouter(options.nextDir || 'src/routes.tsx');
|
|
66
|
-
break;
|
|
67
|
-
case 'node-http':
|
|
68
|
-
case 'unknown':
|
|
69
|
-
default:
|
|
70
|
-
console.warn(`⚠️ Framework "${framework}" not supported yet`);
|
|
71
|
-
console.log('💡 Supported: express, nextjs-pages, nextjs-app, vite, react-router');
|
|
72
|
-
console.log('💡 Tip: Specify framework manually: { framework: "express" }');
|
|
73
|
-
return [];
|
|
151
|
+
}
|
|
74
152
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
routes
|
|
81
|
-
});
|
|
153
|
+
catch (err) {
|
|
154
|
+
const msg = err.message || 'Unknown error during route extraction';
|
|
155
|
+
console.error(`❌ APIForge analysis error: ${msg}`);
|
|
156
|
+
writeStatusFile('error', { error: msg });
|
|
157
|
+
return [];
|
|
82
158
|
}
|
|
83
|
-
|
|
159
|
+
if (routes.length === 0) {
|
|
160
|
+
const msg = 'No routes found — ensure routes are registered before analyze() is called';
|
|
161
|
+
console.warn(`⚠️ APIForge: ${msg}`);
|
|
162
|
+
writeStatusFile('error', { error: msg });
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
console.log(`✅ Found ${routes.length} routes`);
|
|
84
166
|
if (options.saveJson) {
|
|
85
|
-
|
|
86
|
-
|
|
167
|
+
await exportToJSON(routes, options.projectName, options.jsonPath || 'api-export.json', options.baseUrl);
|
|
168
|
+
}
|
|
169
|
+
// Stamp service name on routes if running in microservices mode
|
|
170
|
+
if (options.service) {
|
|
171
|
+
routes = routes.map(r => ({ ...r, service: options.service }));
|
|
172
|
+
}
|
|
173
|
+
// Multi-service collection mode: write routes to a per-service file for the CLI to aggregate.
|
|
174
|
+
// The CLI spawns each service individually, collects via this file, then uploads once.
|
|
175
|
+
if (options.statusFile) {
|
|
176
|
+
try {
|
|
177
|
+
writeFileSync(join(process.cwd(), options.statusFile), JSON.stringify({ status: 'collected', service: options.service, routes, routeCount: routes.length }), 'utf-8');
|
|
178
|
+
}
|
|
179
|
+
catch { }
|
|
180
|
+
return routes;
|
|
87
181
|
}
|
|
88
|
-
// Upload to APIForge
|
|
89
182
|
if (options.upload !== false && options.apiKey) {
|
|
90
183
|
await uploadRoutes(routes, {
|
|
91
184
|
projectName: options.projectName,
|
|
92
185
|
apiKey: options.apiKey,
|
|
93
186
|
baseUrl: options.baseUrl,
|
|
94
187
|
apiForgeUrl: options.apiForgeUrl,
|
|
188
|
+
architectureType: options.architectureType,
|
|
95
189
|
});
|
|
96
190
|
}
|
|
97
191
|
return routes;
|
|
98
192
|
}
|
|
99
|
-
// Re-
|
|
100
|
-
export { analyzeExpress, analyzeNextPages, analyzeNextApp, analyzeVite, analyzeReactRouter };
|
|
193
|
+
// Re-exports for library consumers
|
|
194
|
+
export { analyzeExpress, analyzeNextPages, analyzeNextApp, analyzeVite, analyzeReactRouter, analyzeNestJS };
|
|
101
195
|
export { exportToJSON } from './export.js';
|
|
102
196
|
export { uploadRoutes } from './uploader.js';
|
|
103
197
|
export { detectFramework } from './detect.js';
|
|
104
|
-
|
|
198
|
+
export { detectArchitectureType } from './arch-detect.js';
|
|
105
199
|
export default {
|
|
106
200
|
analyze,
|
|
107
201
|
analyzeExpress,
|
|
@@ -109,7 +203,9 @@ export default {
|
|
|
109
203
|
analyzeNextApp,
|
|
110
204
|
analyzeVite,
|
|
111
205
|
analyzeReactRouter,
|
|
206
|
+
analyzeNestJS,
|
|
112
207
|
exportToJSON,
|
|
113
208
|
uploadRoutes,
|
|
114
|
-
detectFramework
|
|
209
|
+
detectFramework,
|
|
210
|
+
detectArchitectureType,
|
|
115
211
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Detect Express feature-module structure by scanning standard directory patterns.
|
|
5
|
+
* Works for: src/modules/, src/features/, src/domains/ conventions.
|
|
6
|
+
*/
|
|
7
|
+
export function detectExpressModules(rootDir) {
|
|
8
|
+
const modules = [];
|
|
9
|
+
const srcDir = join(rootDir, 'src');
|
|
10
|
+
for (const parentDir of ['modules', 'features', 'domains']) {
|
|
11
|
+
const dir = existsSync(srcDir)
|
|
12
|
+
? join(srcDir, parentDir)
|
|
13
|
+
: join(rootDir, parentDir);
|
|
14
|
+
if (!existsSync(dir))
|
|
15
|
+
continue;
|
|
16
|
+
const subdirs = readdirSync(dir, { withFileTypes: true })
|
|
17
|
+
.filter(entry => entry.isDirectory())
|
|
18
|
+
.map(entry => entry.name);
|
|
19
|
+
for (const name of subdirs) {
|
|
20
|
+
const routePrefix = `/${name}`;
|
|
21
|
+
modules.push({ name, routePrefix });
|
|
22
|
+
}
|
|
23
|
+
if (modules.length > 0)
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
return modules;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Given a route path, return the module name it belongs to.
|
|
30
|
+
* Falls back to the tag (first meaningful path segment) when no module matches.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveModuleForPath(routePath, modules) {
|
|
33
|
+
if (modules.length === 0)
|
|
34
|
+
return undefined;
|
|
35
|
+
// Strip /api/v1/ etc. prefixes before matching
|
|
36
|
+
const normalized = routePath.replace(/^\/(api\/)?(v\d+\/)?/, '/');
|
|
37
|
+
for (const mod of modules) {
|
|
38
|
+
if (normalized.startsWith(mod.routePrefix + '/') || normalized === mod.routePrefix) {
|
|
39
|
+
return mod.name;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Fallback: first non-param, non-version segment
|
|
43
|
+
const segment = normalized.split('/').filter(s => s && !s.startsWith(':'))[0];
|
|
44
|
+
return segment || undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Annotate a list of routes with their module names using path-prefix matching.
|
|
48
|
+
*/
|
|
49
|
+
export function annotateRoutesWithModules(routes, modules) {
|
|
50
|
+
return routes.map(route => ({
|
|
51
|
+
...route,
|
|
52
|
+
module: resolveModuleForPath(route.path, modules) || inferModuleFromPath(route.path),
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
function inferModuleFromPath(routePath) {
|
|
56
|
+
const skip = new Set(['api', 'v1', 'v2', 'v3']);
|
|
57
|
+
return (routePath
|
|
58
|
+
.split('/')
|
|
59
|
+
.filter(s => s && !s.startsWith(':') && !skip.has(s))[0] || 'general');
|
|
60
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'fs';
|
|
2
|
+
import { join, basename } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Detect NestJS modules by scanning for *.module.ts files and extracting
|
|
5
|
+
* their declared controllers with the @Module() decorator.
|
|
6
|
+
*/
|
|
7
|
+
export function detectNestModules(rootDir) {
|
|
8
|
+
const srcDir = join(rootDir, 'src');
|
|
9
|
+
const searchDir = existsSync(srcDir) ? srcDir : rootDir;
|
|
10
|
+
const moduleFiles = findModuleFiles(searchDir);
|
|
11
|
+
if (moduleFiles.length === 0)
|
|
12
|
+
return [];
|
|
13
|
+
const modules = [];
|
|
14
|
+
for (const filePath of moduleFiles) {
|
|
15
|
+
const name = basename(filePath).replace('.module.ts', '');
|
|
16
|
+
// Skip the root AppModule — it's the container, not a domain module
|
|
17
|
+
if (name === 'app')
|
|
18
|
+
continue;
|
|
19
|
+
const routePrefix = inferPrefixFromModuleName(name);
|
|
20
|
+
modules.push({ name, routePrefix });
|
|
21
|
+
}
|
|
22
|
+
return modules;
|
|
23
|
+
}
|
|
24
|
+
function findModuleFiles(dir, depth = 0) {
|
|
25
|
+
if (depth > 8)
|
|
26
|
+
return [];
|
|
27
|
+
const results = [];
|
|
28
|
+
try {
|
|
29
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (['node_modules', 'dist', '.next'].includes(entry.name))
|
|
32
|
+
continue;
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
results.push(...findModuleFiles(join(dir, entry.name), depth + 1));
|
|
35
|
+
}
|
|
36
|
+
else if (entry.isFile() && entry.name.endsWith('.module.ts')) {
|
|
37
|
+
results.push(join(dir, entry.name));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
return results;
|
|
43
|
+
}
|
|
44
|
+
function inferPrefixFromModuleName(name) {
|
|
45
|
+
// user-profile → /user-profile, users → /users
|
|
46
|
+
return `/${name}`;
|
|
47
|
+
}
|
package/dist/esm/uploader.js
CHANGED
|
@@ -1,28 +1,66 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
+
import { writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
2
4
|
import { enrichRoutes } from './export.js';
|
|
5
|
+
/** Status file written to the project root so the CLI can detect completion. */
|
|
6
|
+
const STATUS_FILE = '.apiforge-status.json';
|
|
7
|
+
export function writeStatusFile(status, data) {
|
|
8
|
+
try {
|
|
9
|
+
writeFileSync(join(process.cwd(), STATUS_FILE), JSON.stringify({ status, timestamp: Date.now(), ...data }), 'utf-8');
|
|
10
|
+
}
|
|
11
|
+
catch { }
|
|
12
|
+
}
|
|
13
|
+
export function removeStatusFile() {
|
|
14
|
+
try {
|
|
15
|
+
const p = join(process.cwd(), STATUS_FILE);
|
|
16
|
+
if (existsSync(p))
|
|
17
|
+
unlinkSync(p);
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
}
|
|
3
21
|
/**
|
|
4
|
-
*
|
|
22
|
+
* Submit routes to the APIForge backend for user review.
|
|
23
|
+
* Returns the pendingImportId so the CLI can poll for approval.
|
|
5
24
|
*/
|
|
6
25
|
export async function uploadRoutes(routes, options) {
|
|
7
|
-
|
|
26
|
+
// Production URL is the built-in default — no config needed.
|
|
27
|
+
// Override only for local testing via option or env var.
|
|
28
|
+
const backendUrl = (options.apiForgeUrl ||
|
|
29
|
+
process.env.APIFORGE_URL ||
|
|
30
|
+
'https://apiforgeapi.brainfogagency.com').replace(/\/$/, '');
|
|
8
31
|
const enrichedRoutes = enrichRoutes(routes);
|
|
32
|
+
const payload = {
|
|
33
|
+
projectName: options.projectName,
|
|
34
|
+
routes: enrichedRoutes,
|
|
35
|
+
baseUrl: options.baseUrl,
|
|
36
|
+
framework: enrichedRoutes[0]?.framework || 'unknown',
|
|
37
|
+
architectureType: options.architectureType || 'monolithic',
|
|
38
|
+
services: options.services || [],
|
|
39
|
+
};
|
|
9
40
|
try {
|
|
10
|
-
const response = await axios.post(`${
|
|
11
|
-
projectName: options.projectName,
|
|
12
|
-
routes: enrichedRoutes,
|
|
13
|
-
baseUrl: options.baseUrl,
|
|
14
|
-
framework: enrichedRoutes[0]?.framework || 'unknown',
|
|
15
|
-
}, {
|
|
41
|
+
const response = await axios.post(`${backendUrl}/api/cli/submit`, payload, {
|
|
16
42
|
headers: {
|
|
17
43
|
'Content-Type': 'application/json',
|
|
18
44
|
'X-API-Key': options.apiKey,
|
|
45
|
+
'User-Agent': 'apiforge-analyzer/1.3.0',
|
|
19
46
|
},
|
|
20
47
|
timeout: 30000,
|
|
21
48
|
});
|
|
22
|
-
|
|
49
|
+
const data = response.data?.data;
|
|
50
|
+
console.log(`✅ APIForge: ${data?.message || 'Routes submitted for review'}`);
|
|
51
|
+
writeStatusFile('submitted', {
|
|
52
|
+
pendingImportId: data.pendingImportId,
|
|
53
|
+
routeCount: data.routeCount,
|
|
54
|
+
});
|
|
55
|
+
return data;
|
|
23
56
|
}
|
|
24
57
|
catch (error) {
|
|
25
|
-
|
|
26
|
-
|
|
58
|
+
const msg = error.response?.data?.message ||
|
|
59
|
+
error.response?.data?.error ||
|
|
60
|
+
error.message ||
|
|
61
|
+
'Unknown error';
|
|
62
|
+
console.error(`❌ APIForge upload failed: ${msg}`);
|
|
63
|
+
writeStatusFile('error', { error: msg });
|
|
64
|
+
return null;
|
|
27
65
|
}
|
|
28
66
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type ArchitectureType = 'monolithic' | 'modular-monolithic' | 'microservices';
|
|
2
|
+
export interface ArchDetectResult {
|
|
3
|
+
type: ArchitectureType;
|
|
4
|
+
confidence: number;
|
|
5
|
+
evidence: string[];
|
|
6
|
+
moduleNames: string[];
|
|
7
|
+
servicePaths: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function detectArchitectureType(rootDir: string): ArchDetectResult;
|
package/dist/types/detect.d.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
* Framework detection utilities
|
|
3
|
-
*/
|
|
4
|
-
export type Framework = 'express' | 'nextjs-pages' | 'nextjs-app' | 'vite' | 'react-router' | 'node-http' | 'unknown';
|
|
1
|
+
export type Framework = 'express' | 'fastify' | 'koa' | 'hapi' | 'nestjs' | 'nextjs-pages' | 'nextjs-app' | 'vite' | 'react-router' | 'node-http' | 'unknown';
|
|
5
2
|
export interface FrameworkInfo {
|
|
6
3
|
name: Framework;
|
|
7
4
|
version?: string;
|
|
8
5
|
confidence: number;
|
|
9
6
|
}
|
|
10
7
|
/**
|
|
11
|
-
* Detect
|
|
8
|
+
* Detect the web framework from the live app instance + filesystem hints.
|
|
9
|
+
* Confidence is used to pick the best match when multiple signals fire.
|
|
12
10
|
*/
|
|
13
11
|
export declare function detectFramework(app?: any): FrameworkInfo;
|
package/dist/types/export.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Express analyzer —
|
|
2
|
+
* Express route analyzer — deep extraction of routes, parameters, and auth patterns
|
|
3
3
|
*/
|
|
4
4
|
export interface Route {
|
|
5
5
|
id?: number;
|
|
@@ -8,6 +8,7 @@ export interface Route {
|
|
|
8
8
|
tag: string;
|
|
9
9
|
operationId: string;
|
|
10
10
|
summary: string;
|
|
11
|
+
description?: string;
|
|
11
12
|
middleware: string[];
|
|
12
13
|
authRequired: boolean;
|
|
13
14
|
pathParams: string[];
|
|
@@ -18,6 +19,5 @@ export interface Route {
|
|
|
18
19
|
framework: string;
|
|
19
20
|
responseStatuses?: number[];
|
|
20
21
|
headerParams?: string[];
|
|
21
|
-
description?: string;
|
|
22
22
|
}
|
|
23
23
|
export declare function analyzeExpress(app: any): Route[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Route } from '../export.js';
|
|
2
|
+
/**
|
|
3
|
+
* Analyze a NestJS application by extracting the underlying Express (or Fastify)
|
|
4
|
+
* HTTP adapter and reusing the existing Express route-walker.
|
|
5
|
+
*
|
|
6
|
+
* NestJS wraps an inner Express app under app.getHttpServer()._events.request.
|
|
7
|
+
* When that's available we get full runtime accuracy. Otherwise we warn and
|
|
8
|
+
* fall back to an empty array so the CLI can report the limitation.
|
|
9
|
+
*/
|
|
10
|
+
export declare function analyzeNestJS(app: any, rootDir?: string): Promise<Route[]>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* APIForge Analyzer — main entry point (dual ESM/CJS)
|
|
3
3
|
*/
|
|
4
4
|
import { analyzeExpress } from './frameworks/express.js';
|
|
5
5
|
import { analyzeNextPages } from './frameworks/nextjs-pages.js';
|
|
6
6
|
import { analyzeNextApp } from './frameworks/nextjs-app.js';
|
|
7
7
|
import { analyzeVite } from './frameworks/vite.js';
|
|
8
8
|
import { analyzeReactRouter } from './frameworks/react-router.js';
|
|
9
|
+
import { analyzeNestJS } from './frameworks/nestjs.js';
|
|
9
10
|
import { detectFramework } from './detect.js';
|
|
10
11
|
import { exportToJSON } from './export.js';
|
|
11
12
|
import { uploadRoutes } from './uploader.js';
|
|
13
|
+
import { detectArchitectureType } from './arch-detect.js';
|
|
14
|
+
import type { ArchitectureType } from './arch-detect.js';
|
|
12
15
|
export interface AnalyzerOptions {
|
|
13
16
|
projectName: string;
|
|
14
17
|
baseUrl?: string;
|
|
@@ -21,15 +24,17 @@ export interface AnalyzerOptions {
|
|
|
21
24
|
nextDir?: string;
|
|
22
25
|
skipInProduction?: boolean;
|
|
23
26
|
runOnce?: boolean;
|
|
27
|
+
architectureType?: ArchitectureType;
|
|
28
|
+
service?: string;
|
|
29
|
+
/** CLI multi-service mode: write routes to this file and exit without uploading */
|
|
30
|
+
statusFile?: string;
|
|
24
31
|
}
|
|
25
|
-
/**
|
|
26
|
-
* Main analyze function with auto-detection
|
|
27
|
-
*/
|
|
28
32
|
export declare function analyze(app: any, options: AnalyzerOptions): Promise<any[]>;
|
|
29
|
-
export { analyzeExpress, analyzeNextPages, analyzeNextApp, analyzeVite, analyzeReactRouter };
|
|
33
|
+
export { analyzeExpress, analyzeNextPages, analyzeNextApp, analyzeVite, analyzeReactRouter, analyzeNestJS };
|
|
30
34
|
export { exportToJSON } from './export.js';
|
|
31
35
|
export { uploadRoutes } from './uploader.js';
|
|
32
36
|
export { detectFramework } from './detect.js';
|
|
37
|
+
export { detectArchitectureType } from './arch-detect.js';
|
|
33
38
|
declare const _default: {
|
|
34
39
|
analyze: typeof analyze;
|
|
35
40
|
analyzeExpress: typeof analyzeExpress;
|
|
@@ -37,8 +42,10 @@ declare const _default: {
|
|
|
37
42
|
analyzeNextApp: typeof analyzeNextApp;
|
|
38
43
|
analyzeVite: typeof analyzeVite;
|
|
39
44
|
analyzeReactRouter: typeof analyzeReactRouter;
|
|
45
|
+
analyzeNestJS: typeof analyzeNestJS;
|
|
40
46
|
exportToJSON: typeof exportToJSON;
|
|
41
47
|
uploadRoutes: typeof uploadRoutes;
|
|
42
48
|
detectFramework: typeof detectFramework;
|
|
49
|
+
detectArchitectureType: typeof detectArchitectureType;
|
|
43
50
|
};
|
|
44
51
|
export default _default;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ModuleInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
routePrefix: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Detect Express feature-module structure by scanning standard directory patterns.
|
|
7
|
+
* Works for: src/modules/, src/features/, src/domains/ conventions.
|
|
8
|
+
*/
|
|
9
|
+
export declare function detectExpressModules(rootDir: string): ModuleInfo[];
|
|
10
|
+
/**
|
|
11
|
+
* Given a route path, return the module name it belongs to.
|
|
12
|
+
* Falls back to the tag (first meaningful path segment) when no module matches.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveModuleForPath(routePath: string, modules: ModuleInfo[]): string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Annotate a list of routes with their module names using path-prefix matching.
|
|
17
|
+
*/
|
|
18
|
+
export declare function annotateRoutesWithModules<T extends {
|
|
19
|
+
path: string;
|
|
20
|
+
}>(routes: T[], modules: ModuleInfo[]): (T & {
|
|
21
|
+
module: string;
|
|
22
|
+
})[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ModuleInfo } from './express.js';
|
|
2
|
+
/**
|
|
3
|
+
* Detect NestJS modules by scanning for *.module.ts files and extracting
|
|
4
|
+
* their declared controllers with the @Module() decorator.
|
|
5
|
+
*/
|
|
6
|
+
export declare function detectNestModules(rootDir: string): ModuleInfo[];
|
package/dist/types/uploader.d.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { Route } from './export.js';
|
|
2
|
+
import type { ArchitectureType } from './arch-detect.js';
|
|
3
|
+
export interface ServiceMeta {
|
|
4
|
+
name: string;
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
}
|
|
2
7
|
export interface UploadOptions {
|
|
3
8
|
projectName: string;
|
|
4
9
|
baseUrl?: string;
|
|
5
10
|
apiForgeUrl?: string;
|
|
6
11
|
apiKey: string;
|
|
12
|
+
architectureType?: ArchitectureType;
|
|
13
|
+
services?: ServiceMeta[];
|
|
14
|
+
}
|
|
15
|
+
export interface UploadResult {
|
|
16
|
+
pendingImportId: string;
|
|
17
|
+
routeCount: number;
|
|
18
|
+
message: string;
|
|
7
19
|
}
|
|
20
|
+
export declare function writeStatusFile(status: 'submitted' | 'error', data: Record<string, any>): void;
|
|
21
|
+
export declare function removeStatusFile(): void;
|
|
8
22
|
/**
|
|
9
|
-
*
|
|
23
|
+
* Submit routes to the APIForge backend for user review.
|
|
24
|
+
* Returns the pendingImportId so the CLI can poll for approval.
|
|
10
25
|
*/
|
|
11
|
-
export declare function uploadRoutes(routes: Route[], options: UploadOptions): Promise<
|
|
26
|
+
export declare function uploadRoutes(routes: Route[], options: UploadOptions): Promise<UploadResult | null>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apiforge-analyzer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Universal API documentation generator for Express, Next.js, Vite, React Router, and Node.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"build:esm": "tsc -p tsconfig.esm.json",
|
|
27
27
|
"build:cjs": "tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
|
|
28
28
|
"build:types": "tsc -p tsconfig.types.json",
|
|
29
|
-
"prepublishOnly": "npm run build"
|
|
29
|
+
"prepublishOnly": "npm run build",
|
|
30
|
+
"test": "node ../test-apps/run-all.cjs"
|
|
30
31
|
},
|
|
31
32
|
"keywords": [
|
|
32
33
|
"api",
|