apiforge-analyzer 1.2.1 → 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.
@@ -0,0 +1,35 @@
1
+ import { analyzeExpress } from './express.js';
2
+ import { detectNestModules } from '../modules/nestjs.js';
3
+ import { detectExpressModules, annotateRoutesWithModules } from '../modules/express.js';
4
+ /**
5
+ * Analyze a NestJS application by extracting the underlying Express (or Fastify)
6
+ * HTTP adapter and reusing the existing Express route-walker.
7
+ *
8
+ * NestJS wraps an inner Express app under app.getHttpServer()._events.request.
9
+ * When that's available we get full runtime accuracy. Otherwise we warn and
10
+ * fall back to an empty array so the CLI can report the limitation.
11
+ */
12
+ export async function analyzeNestJS(app, rootDir) {
13
+ if (!app || typeof app.getHttpServer !== 'function') {
14
+ console.warn('⚠️ APIForge: NestJS app instance required. Pass the Nest application object.');
15
+ return [];
16
+ }
17
+ const httpServer = app.getHttpServer();
18
+ // NestJS wraps Express under _events.request when using the default adapter
19
+ const innerApp = httpServer?._events?.request ||
20
+ httpServer?._events?.['request'] ||
21
+ null;
22
+ if (!innerApp || typeof innerApp !== 'function') {
23
+ console.warn('⚠️ APIForge: Could not access inner Express adapter from NestJS app.');
24
+ return [];
25
+ }
26
+ const routes = await analyzeExpress(innerApp);
27
+ if (routes.length === 0)
28
+ return [];
29
+ // Detect module structure and annotate each route
30
+ const dir = rootDir || process.cwd();
31
+ const modules = detectNestModules(dir).length > 0
32
+ ? detectNestModules(dir)
33
+ : detectExpressModules(dir);
34
+ return annotateRoutesWithModules(routes, modules);
35
+ }
package/dist/esm/index.js CHANGED
@@ -8,9 +8,13 @@ import { analyzeNextPages } from './frameworks/nextjs-pages.js';
8
8
  import { analyzeNextApp } from './frameworks/nextjs-app.js';
9
9
  import { analyzeVite } from './frameworks/vite.js';
10
10
  import { analyzeReactRouter } from './frameworks/react-router.js';
11
+ import { analyzeNestJS } from './frameworks/nestjs.js';
11
12
  import { detectFramework } from './detect.js';
12
13
  import { exportToJSON } from './export.js';
13
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';
14
18
  // ─── Env file loader — no dotenv dependency ───────────────────────────────────
15
19
  /**
16
20
  * Read a single variable from a .env-style file.
@@ -66,8 +70,10 @@ export async function analyze(app, options) {
66
70
  if (options.skipInProduction !== false && process.env.NODE_ENV === 'production') {
67
71
  return [];
68
72
  }
69
- // File-based debounce — survives nodemon restarts
70
- if (options.runOnce !== false) {
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) {
71
77
  const elapsed = Date.now() - readLastRunMs();
72
78
  if (elapsed < DEBOUNCE_MS) {
73
79
  console.log(`⚡ APIForge: skipped (last run ${Math.round(elapsed / 1000)}s ago)`);
@@ -103,6 +109,9 @@ export async function analyze(app, options) {
103
109
  }
104
110
  try {
105
111
  switch (framework) {
112
+ case 'nestjs':
113
+ routes = await analyzeNestJS(app, options.nextDir || process.cwd());
114
+ break;
106
115
  case 'express':
107
116
  if (!app) {
108
117
  const msg = 'Express app instance required — pass your app object to analyze()';
@@ -111,6 +120,16 @@ export async function analyze(app, options) {
111
120
  return [];
112
121
  }
113
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
+ }
114
133
  break;
115
134
  case 'nextjs-pages':
116
135
  routes = await analyzeNextPages(options.nextDir || 'pages/api');
@@ -147,21 +166,36 @@ export async function analyze(app, options) {
147
166
  if (options.saveJson) {
148
167
  await exportToJSON(routes, options.projectName, options.jsonPath || 'api-export.json', options.baseUrl);
149
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;
181
+ }
150
182
  if (options.upload !== false && options.apiKey) {
151
183
  await uploadRoutes(routes, {
152
184
  projectName: options.projectName,
153
185
  apiKey: options.apiKey,
154
186
  baseUrl: options.baseUrl,
155
187
  apiForgeUrl: options.apiForgeUrl,
188
+ architectureType: options.architectureType,
156
189
  });
157
190
  }
158
191
  return routes;
159
192
  }
160
193
  // Re-exports for library consumers
161
- export { analyzeExpress, analyzeNextPages, analyzeNextApp, analyzeVite, analyzeReactRouter };
194
+ export { analyzeExpress, analyzeNextPages, analyzeNextApp, analyzeVite, analyzeReactRouter, analyzeNestJS };
162
195
  export { exportToJSON } from './export.js';
163
196
  export { uploadRoutes } from './uploader.js';
164
197
  export { detectFramework } from './detect.js';
198
+ export { detectArchitectureType } from './arch-detect.js';
165
199
  export default {
166
200
  analyze,
167
201
  analyzeExpress,
@@ -169,7 +203,9 @@ export default {
169
203
  analyzeNextApp,
170
204
  analyzeVite,
171
205
  analyzeReactRouter,
206
+ analyzeNestJS,
172
207
  exportToJSON,
173
208
  uploadRoutes,
174
209
  detectFramework,
210
+ detectArchitectureType,
175
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
+ }
@@ -34,13 +34,15 @@ export async function uploadRoutes(routes, options) {
34
34
  routes: enrichedRoutes,
35
35
  baseUrl: options.baseUrl,
36
36
  framework: enrichedRoutes[0]?.framework || 'unknown',
37
+ architectureType: options.architectureType || 'monolithic',
38
+ services: options.services || [],
37
39
  };
38
40
  try {
39
41
  const response = await axios.post(`${backendUrl}/api/cli/submit`, payload, {
40
42
  headers: {
41
43
  'Content-Type': 'application/json',
42
44
  'X-API-Key': options.apiKey,
43
- 'User-Agent': 'apiforge-analyzer/1.2.0',
45
+ 'User-Agent': 'apiforge-analyzer/1.3.0',
44
46
  },
45
47
  timeout: 30000,
46
48
  });
@@ -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;
@@ -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
  /**
@@ -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[]>;
@@ -6,9 +6,12 @@ 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,12 +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
32
  export declare function analyze(app: any, options: AnalyzerOptions): Promise<any[]>;
26
- export { analyzeExpress, analyzeNextPages, analyzeNextApp, analyzeVite, analyzeReactRouter };
33
+ export { analyzeExpress, analyzeNextPages, analyzeNextApp, analyzeVite, analyzeReactRouter, analyzeNestJS };
27
34
  export { exportToJSON } from './export.js';
28
35
  export { uploadRoutes } from './uploader.js';
29
36
  export { detectFramework } from './detect.js';
37
+ export { detectArchitectureType } from './arch-detect.js';
30
38
  declare const _default: {
31
39
  analyze: typeof analyze;
32
40
  analyzeExpress: typeof analyzeExpress;
@@ -34,8 +42,10 @@ declare const _default: {
34
42
  analyzeNextApp: typeof analyzeNextApp;
35
43
  analyzeVite: typeof analyzeVite;
36
44
  analyzeReactRouter: typeof analyzeReactRouter;
45
+ analyzeNestJS: typeof analyzeNestJS;
37
46
  exportToJSON: typeof exportToJSON;
38
47
  uploadRoutes: typeof uploadRoutes;
39
48
  detectFramework: typeof detectFramework;
49
+ detectArchitectureType: typeof detectArchitectureType;
40
50
  };
41
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,9 +1,16 @@
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[];
7
14
  }
8
15
  export interface UploadResult {
9
16
  pendingImportId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiforge-analyzer",
3
- "version": "1.2.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",