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,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeNestJS = analyzeNestJS;
4
+ const express_js_1 = require("./express.js");
5
+ const nestjs_js_1 = require("../modules/nestjs.js");
6
+ const express_js_2 = require("../modules/express.js");
7
+ /**
8
+ * Analyze a NestJS application by extracting the underlying Express (or Fastify)
9
+ * HTTP adapter and reusing the existing Express route-walker.
10
+ *
11
+ * NestJS wraps an inner Express app under app.getHttpServer()._events.request.
12
+ * When that's available we get full runtime accuracy. Otherwise we warn and
13
+ * fall back to an empty array so the CLI can report the limitation.
14
+ */
15
+ async function analyzeNestJS(app, rootDir) {
16
+ if (!app || typeof app.getHttpServer !== 'function') {
17
+ console.warn('⚠️ APIForge: NestJS app instance required. Pass the Nest application object.');
18
+ return [];
19
+ }
20
+ const httpServer = app.getHttpServer();
21
+ // NestJS wraps Express under _events.request when using the default adapter
22
+ const innerApp = httpServer?._events?.request ||
23
+ httpServer?._events?.['request'] ||
24
+ null;
25
+ if (!innerApp || typeof innerApp !== 'function') {
26
+ console.warn('⚠️ APIForge: Could not access inner Express adapter from NestJS app.');
27
+ return [];
28
+ }
29
+ const routes = await (0, express_js_1.analyzeExpress)(innerApp);
30
+ if (routes.length === 0)
31
+ return [];
32
+ // Detect module structure and annotate each route
33
+ const dir = rootDir || process.cwd();
34
+ const modules = (0, nestjs_js_1.detectNestModules)(dir).length > 0
35
+ ? (0, nestjs_js_1.detectNestModules)(dir)
36
+ : (0, express_js_2.detectExpressModules)(dir);
37
+ return (0, express_js_2.annotateRoutesWithModules)(routes, modules);
38
+ }
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.detectFramework = exports.uploadRoutes = exports.exportToJSON = exports.analyzeReactRouter = exports.analyzeVite = exports.analyzeNextApp = exports.analyzeNextPages = exports.analyzeExpress = void 0;
6
+ exports.detectArchitectureType = exports.detectFramework = exports.uploadRoutes = exports.exportToJSON = 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");
@@ -17,9 +17,14 @@ const vite_js_1 = require("./frameworks/vite.js");
17
17
  Object.defineProperty(exports, "analyzeVite", { enumerable: true, get: function () { return vite_js_1.analyzeVite; } });
18
18
  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
+ const nestjs_js_1 = require("./frameworks/nestjs.js");
21
+ Object.defineProperty(exports, "analyzeNestJS", { enumerable: true, get: function () { return nestjs_js_1.analyzeNestJS; } });
20
22
  const detect_js_1 = require("./detect.js");
21
23
  const export_js_1 = require("./export.js");
22
24
  const uploader_js_1 = require("./uploader.js");
25
+ const arch_detect_js_1 = require("./arch-detect.js");
26
+ const express_js_2 = require("./modules/express.js");
27
+ const nestjs_js_2 = require("./modules/nestjs.js");
23
28
  // ─── Env file loader — no dotenv dependency ───────────────────────────────────
24
29
  /**
25
30
  * Read a single variable from a .env-style file.
@@ -75,8 +80,10 @@ async function analyze(app, options) {
75
80
  if (options.skipInProduction !== false && process.env.NODE_ENV === 'production') {
76
81
  return [];
77
82
  }
78
- // File-based debounce — survives nodemon restarts
79
- if (options.runOnce !== false) {
83
+ // File-based debounce — survives nodemon restarts.
84
+ // Skipped in collection mode (statusFile set) because the CLI controls each service start
85
+ // and the shared .apiforge-last-run.json would cause services 2+ to be skipped.
86
+ if (options.runOnce !== false && !options.statusFile) {
80
87
  const elapsed = Date.now() - readLastRunMs();
81
88
  if (elapsed < DEBOUNCE_MS) {
82
89
  console.log(`⚡ APIForge: skipped (last run ${Math.round(elapsed / 1000)}s ago)`);
@@ -112,6 +119,9 @@ async function analyze(app, options) {
112
119
  }
113
120
  try {
114
121
  switch (framework) {
122
+ case 'nestjs':
123
+ routes = await (0, nestjs_js_1.analyzeNestJS)(app, options.nextDir || process.cwd());
124
+ break;
115
125
  case 'express':
116
126
  if (!app) {
117
127
  const msg = 'Express app instance required — pass your app object to analyze()';
@@ -120,6 +130,16 @@ async function analyze(app, options) {
120
130
  return [];
121
131
  }
122
132
  routes = await (0, express_js_1.analyzeExpress)(app);
133
+ // For modular-monolithic: annotate each route with its module name
134
+ if (options.architectureType === 'modular-monolithic') {
135
+ const dir = options.nextDir || process.cwd();
136
+ const modules = (0, nestjs_js_2.detectNestModules)(dir).length > 0
137
+ ? (0, nestjs_js_2.detectNestModules)(dir)
138
+ : (0, express_js_2.detectExpressModules)(dir);
139
+ if (modules.length > 0) {
140
+ routes = (0, express_js_2.annotateRoutesWithModules)(routes, modules);
141
+ }
142
+ }
123
143
  break;
124
144
  case 'nextjs-pages':
125
145
  routes = await (0, nextjs_pages_js_1.analyzeNextPages)(options.nextDir || 'pages/api');
@@ -156,12 +176,26 @@ async function analyze(app, options) {
156
176
  if (options.saveJson) {
157
177
  await (0, export_js_1.exportToJSON)(routes, options.projectName, options.jsonPath || 'api-export.json', options.baseUrl);
158
178
  }
179
+ // Stamp service name on routes if running in microservices mode
180
+ if (options.service) {
181
+ routes = routes.map(r => ({ ...r, service: options.service }));
182
+ }
183
+ // Multi-service collection mode: write routes to a per-service file for the CLI to aggregate.
184
+ // The CLI spawns each service individually, collects via this file, then uploads once.
185
+ if (options.statusFile) {
186
+ try {
187
+ (0, fs_1.writeFileSync)((0, path_1.join)(process.cwd(), options.statusFile), JSON.stringify({ status: 'collected', service: options.service, routes, routeCount: routes.length }), 'utf-8');
188
+ }
189
+ catch { }
190
+ return routes;
191
+ }
159
192
  if (options.upload !== false && options.apiKey) {
160
193
  await (0, uploader_js_1.uploadRoutes)(routes, {
161
194
  projectName: options.projectName,
162
195
  apiKey: options.apiKey,
163
196
  baseUrl: options.baseUrl,
164
197
  apiForgeUrl: options.apiForgeUrl,
198
+ architectureType: options.architectureType,
165
199
  });
166
200
  }
167
201
  return routes;
@@ -172,6 +206,8 @@ var uploader_js_2 = require("./uploader.js");
172
206
  Object.defineProperty(exports, "uploadRoutes", { enumerable: true, get: function () { return uploader_js_2.uploadRoutes; } });
173
207
  var detect_js_2 = require("./detect.js");
174
208
  Object.defineProperty(exports, "detectFramework", { enumerable: true, get: function () { return detect_js_2.detectFramework; } });
209
+ var arch_detect_js_2 = require("./arch-detect.js");
210
+ Object.defineProperty(exports, "detectArchitectureType", { enumerable: true, get: function () { return arch_detect_js_2.detectArchitectureType; } });
175
211
  exports.default = {
176
212
  analyze,
177
213
  analyzeExpress: express_js_1.analyzeExpress,
@@ -179,7 +215,9 @@ exports.default = {
179
215
  analyzeNextApp: nextjs_app_js_1.analyzeNextApp,
180
216
  analyzeVite: vite_js_1.analyzeVite,
181
217
  analyzeReactRouter: react_router_js_1.analyzeReactRouter,
218
+ analyzeNestJS: nestjs_js_1.analyzeNestJS,
182
219
  exportToJSON: export_js_1.exportToJSON,
183
220
  uploadRoutes: uploader_js_1.uploadRoutes,
184
221
  detectFramework: detect_js_1.detectFramework,
222
+ detectArchitectureType: arch_detect_js_1.detectArchitectureType,
185
223
  };
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectExpressModules = detectExpressModules;
4
+ exports.resolveModuleForPath = resolveModuleForPath;
5
+ exports.annotateRoutesWithModules = annotateRoutesWithModules;
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ /**
9
+ * Detect Express feature-module structure by scanning standard directory patterns.
10
+ * Works for: src/modules/, src/features/, src/domains/ conventions.
11
+ */
12
+ function detectExpressModules(rootDir) {
13
+ const modules = [];
14
+ const srcDir = (0, path_1.join)(rootDir, 'src');
15
+ for (const parentDir of ['modules', 'features', 'domains']) {
16
+ const dir = (0, fs_1.existsSync)(srcDir)
17
+ ? (0, path_1.join)(srcDir, parentDir)
18
+ : (0, path_1.join)(rootDir, parentDir);
19
+ if (!(0, fs_1.existsSync)(dir))
20
+ continue;
21
+ const subdirs = (0, fs_1.readdirSync)(dir, { withFileTypes: true })
22
+ .filter(entry => entry.isDirectory())
23
+ .map(entry => entry.name);
24
+ for (const name of subdirs) {
25
+ const routePrefix = `/${name}`;
26
+ modules.push({ name, routePrefix });
27
+ }
28
+ if (modules.length > 0)
29
+ break;
30
+ }
31
+ return modules;
32
+ }
33
+ /**
34
+ * Given a route path, return the module name it belongs to.
35
+ * Falls back to the tag (first meaningful path segment) when no module matches.
36
+ */
37
+ function resolveModuleForPath(routePath, modules) {
38
+ if (modules.length === 0)
39
+ return undefined;
40
+ // Strip /api/v1/ etc. prefixes before matching
41
+ const normalized = routePath.replace(/^\/(api\/)?(v\d+\/)?/, '/');
42
+ for (const mod of modules) {
43
+ if (normalized.startsWith(mod.routePrefix + '/') || normalized === mod.routePrefix) {
44
+ return mod.name;
45
+ }
46
+ }
47
+ // Fallback: first non-param, non-version segment
48
+ const segment = normalized.split('/').filter(s => s && !s.startsWith(':'))[0];
49
+ return segment || undefined;
50
+ }
51
+ /**
52
+ * Annotate a list of routes with their module names using path-prefix matching.
53
+ */
54
+ function annotateRoutesWithModules(routes, modules) {
55
+ return routes.map(route => ({
56
+ ...route,
57
+ module: resolveModuleForPath(route.path, modules) || inferModuleFromPath(route.path),
58
+ }));
59
+ }
60
+ function inferModuleFromPath(routePath) {
61
+ const skip = new Set(['api', 'v1', 'v2', 'v3']);
62
+ return (routePath
63
+ .split('/')
64
+ .filter(s => s && !s.startsWith(':') && !skip.has(s))[0] || 'general');
65
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectNestModules = detectNestModules;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ /**
7
+ * Detect NestJS modules by scanning for *.module.ts files and extracting
8
+ * their declared controllers with the @Module() decorator.
9
+ */
10
+ function detectNestModules(rootDir) {
11
+ const srcDir = (0, path_1.join)(rootDir, 'src');
12
+ const searchDir = (0, fs_1.existsSync)(srcDir) ? srcDir : rootDir;
13
+ const moduleFiles = findModuleFiles(searchDir);
14
+ if (moduleFiles.length === 0)
15
+ return [];
16
+ const modules = [];
17
+ for (const filePath of moduleFiles) {
18
+ const name = (0, path_1.basename)(filePath).replace('.module.ts', '');
19
+ // Skip the root AppModule — it's the container, not a domain module
20
+ if (name === 'app')
21
+ continue;
22
+ const routePrefix = inferPrefixFromModuleName(name);
23
+ modules.push({ name, routePrefix });
24
+ }
25
+ return modules;
26
+ }
27
+ function findModuleFiles(dir, depth = 0) {
28
+ if (depth > 8)
29
+ return [];
30
+ const results = [];
31
+ try {
32
+ const entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
33
+ for (const entry of entries) {
34
+ if (['node_modules', 'dist', '.next'].includes(entry.name))
35
+ continue;
36
+ if (entry.isDirectory()) {
37
+ results.push(...findModuleFiles((0, path_1.join)(dir, entry.name), depth + 1));
38
+ }
39
+ else if (entry.isFile() && entry.name.endsWith('.module.ts')) {
40
+ results.push((0, path_1.join)(dir, entry.name));
41
+ }
42
+ }
43
+ }
44
+ catch { }
45
+ return results;
46
+ }
47
+ function inferPrefixFromModuleName(name) {
48
+ // user-profile → /user-profile, users → /users
49
+ return `/${name}`;
50
+ }
@@ -42,13 +42,15 @@ async function uploadRoutes(routes, options) {
42
42
  routes: enrichedRoutes,
43
43
  baseUrl: options.baseUrl,
44
44
  framework: enrichedRoutes[0]?.framework || 'unknown',
45
+ architectureType: options.architectureType || 'monolithic',
46
+ services: options.services || [],
45
47
  };
46
48
  try {
47
49
  const response = await axios_1.default.post(`${backendUrl}/api/cli/submit`, payload, {
48
50
  headers: {
49
51
  'Content-Type': 'application/json',
50
52
  'X-API-Key': options.apiKey,
51
- 'User-Agent': 'apiforge-analyzer/1.2.0',
53
+ 'User-Agent': 'apiforge-analyzer/1.3.0',
52
54
  },
53
55
  timeout: 30000,
54
56
  });
@@ -0,0 +1,130 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'fs';
2
+ import { join, basename } from 'path';
3
+ export function detectArchitectureType(rootDir) {
4
+ let microScore = 0;
5
+ let modularScore = 0;
6
+ const evidence = [];
7
+ const servicePaths = [];
8
+ const moduleNames = [];
9
+ // ── Microservices signals ────────────────────────────────────────────────────
10
+ // Definitive: config file lists services[]
11
+ const configPaths = [join(rootDir, 'apiforge.config.ts'), join(rootDir, '.apiforge.json')];
12
+ for (const configPath of configPaths) {
13
+ if (existsSync(configPath)) {
14
+ try {
15
+ const raw = readFileSync(configPath, 'utf-8');
16
+ if (raw.includes('"services"') || raw.includes("'services'") || raw.includes('services:')) {
17
+ microScore += 50;
18
+ evidence.push(`Config file with services[]: ${basename(configPath)}`);
19
+ }
20
+ }
21
+ catch { }
22
+ }
23
+ }
24
+ // docker-compose.yml with multiple node service entries
25
+ const composePath = join(rootDir, 'docker-compose.yml');
26
+ if (existsSync(composePath)) {
27
+ try {
28
+ const compose = readFileSync(composePath, 'utf-8');
29
+ const serviceMatches = compose.match(/^\s{2}\w[\w-]+:/gm) || [];
30
+ if (serviceMatches.length >= 3) {
31
+ microScore += 35;
32
+ evidence.push(`docker-compose.yml with ${serviceMatches.length} services`);
33
+ }
34
+ }
35
+ catch { }
36
+ }
37
+ // Multiple sibling package.json at depth 1
38
+ const siblingPackages = findSiblingPackages(rootDir);
39
+ if (siblingPackages.length >= 2) {
40
+ microScore += 40;
41
+ siblingPackages.forEach(p => servicePaths.push(p));
42
+ evidence.push(`${siblingPackages.length} sibling package.json files (${siblingPackages.join(', ')})`);
43
+ }
44
+ // ── Modular Monolithic signals ────────────────────────────────────────────────
45
+ // NestJS module files anywhere in src/
46
+ const srcDir = join(rootDir, 'src');
47
+ if (existsSync(srcDir)) {
48
+ const nestModules = findFilesRecursive(srcDir, '.module.ts');
49
+ if (nestModules.length > 0) {
50
+ modularScore += 40;
51
+ nestModules.forEach(f => {
52
+ const name = basename(f).replace('.module.ts', '');
53
+ if (!moduleNames.includes(name))
54
+ moduleNames.push(name);
55
+ });
56
+ evidence.push(`NestJS @Module files: ${nestModules.map(f => basename(f)).join(', ')}`);
57
+ }
58
+ // src/modules/, src/features/, or src/domains/
59
+ for (const dir of ['modules', 'features', 'domains']) {
60
+ if (existsSync(join(srcDir, dir))) {
61
+ modularScore += 35;
62
+ evidence.push(`Feature-based directory: src/${dir}/`);
63
+ const subdirs = readdirSync(join(srcDir, dir), { withFileTypes: true })
64
+ .filter(d => d.isDirectory())
65
+ .map(d => d.name);
66
+ subdirs.forEach(name => { if (!moduleNames.includes(name))
67
+ moduleNames.push(name); });
68
+ break;
69
+ }
70
+ }
71
+ }
72
+ // nest-cli.json at root
73
+ if (existsSync(join(rootDir, 'nest-cli.json'))) {
74
+ modularScore += 20;
75
+ evidence.push('nest-cli.json found');
76
+ }
77
+ // Classify
78
+ let type = 'monolithic';
79
+ let confidence = 0.6;
80
+ if (microScore >= 40) {
81
+ type = 'microservices';
82
+ confidence = Math.min(0.5 + microScore / 100, 0.98);
83
+ }
84
+ else if (modularScore >= 35) {
85
+ type = 'modular-monolithic';
86
+ confidence = Math.min(0.5 + modularScore / 100, 0.95);
87
+ }
88
+ if (evidence.length === 0) {
89
+ evidence.push('Single package.json, flat route structure');
90
+ }
91
+ return { type, confidence, evidence, moduleNames, servicePaths };
92
+ }
93
+ function findSiblingPackages(rootDir) {
94
+ const results = [];
95
+ try {
96
+ const entries = readdirSync(rootDir, { withFileTypes: true });
97
+ for (const entry of entries) {
98
+ if (!entry.isDirectory())
99
+ continue;
100
+ if (['node_modules', '.git', 'dist', '.next', 'build'].includes(entry.name))
101
+ continue;
102
+ const pkgPath = join(rootDir, entry.name, 'package.json');
103
+ if (existsSync(pkgPath)) {
104
+ results.push(`./${entry.name}`);
105
+ }
106
+ }
107
+ }
108
+ catch { }
109
+ return results;
110
+ }
111
+ function findFilesRecursive(dir, suffix, maxDepth = 6, depth = 0) {
112
+ if (depth > maxDepth)
113
+ return [];
114
+ const results = [];
115
+ try {
116
+ const entries = readdirSync(dir, { withFileTypes: true });
117
+ for (const entry of entries) {
118
+ if (entry.name === 'node_modules' || entry.name === 'dist')
119
+ continue;
120
+ if (entry.isDirectory()) {
121
+ results.push(...findFilesRecursive(join(dir, entry.name), suffix, maxDepth, depth + 1));
122
+ }
123
+ else if (entry.isFile() && entry.name.endsWith(suffix)) {
124
+ results.push(join(dir, entry.name));
125
+ }
126
+ }
127
+ }
128
+ catch { }
129
+ return results;
130
+ }