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/dist/esm/index.js CHANGED
@@ -1,107 +1,201 @@
1
1
  /**
2
- * Main analyzer entry point with dual module support
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
- // Global state for debouncing
13
- const analysisState = new Map();
14
- const DEBOUNCE_TIME = 5000;
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
- * Main analyze function with auto-detection
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
- // Skip in production
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
- // Debounce check
25
- if (options.runOnce !== false) {
26
- const cached = analysisState.get(options.projectName);
27
- const now = Date.now();
28
- if (cached && (now - cached.timestamp) < DEBOUNCE_TIME) {
29
- console.log(`⚡ Analysis skipped - already ran ${Math.round((now - cached.timestamp) / 1000)}s ago`);
30
- return cached.routes;
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
- console.log(`🔍 Analyzing ${options.projectName}...`);
34
- // CRITICAL: Prevent restart loops - force saveJson=false when uploading
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
- const detected = detectFramework(app);
43
- framework = detected.name;
107
+ framework = detectFramework(app).name;
44
108
  console.log(`📦 Detected framework: ${framework}`);
45
109
  }
46
- // Analyze based on framework
47
- switch (framework) {
48
- case 'express':
49
- if (!app) {
50
- console.error('❌ Express app instance required');
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
- console.log(`✅ Found ${routes.length} routes`);
76
- // Cache results
77
- if (options.runOnce !== false) {
78
- analysisState.set(options.projectName, {
79
- timestamp: Date.now(),
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
- // Save JSON (only if not uploading)
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
- const filePath = options.jsonPath || 'api-export.json';
86
- await exportToJSON(routes, options.projectName, filePath, options.baseUrl);
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-export framework-specific analyzers
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
- // Default export for CommonJS
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
+ }
@@ -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
- * Upload routes to APIForge server
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
- const url = options.apiForgeUrl || process.env.APIFORGE_URL || 'http://localhost:3000';
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(`${url}/api/upload-routes`, {
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
- console.log('✅ Uploaded to APIForge:', response.data);
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
- console.error('❌ Upload failed:', error.message);
26
- throw error;
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;
@@ -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 which framework is being used
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;
@@ -28,6 +28,8 @@ export interface Route {
28
28
  linkedPaths: string[];
29
29
  hasAuth: boolean;
30
30
  framework: string;
31
+ module?: string;
32
+ service?: string;
31
33
  sourceContext?: Record<string, any>;
32
34
  }
33
35
  /**
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Express analyzer — accurate route + payload extraction
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[]>;
@@ -1,14 +1,17 @@
1
1
  /**
2
- * Main analyzer entry point with dual module support
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[];
@@ -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
- * Upload routes to APIForge server
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<void>;
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.1.0",
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",