apiforge-analyzer 1.2.1 → 1.3.1
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 +154 -0
- package/dist/cjs/cli.js +547 -46
- package/dist/cjs/export.js +2 -0
- package/dist/cjs/frameworks/nestjs.js +38 -0
- package/dist/cjs/index.js +41 -3
- package/dist/cjs/modules/express.js +65 -0
- package/dist/cjs/modules/nestjs.js +50 -0
- package/dist/cjs/uploader.js +3 -1
- package/dist/esm/arch-detect.js +151 -0
- package/dist/esm/cli.js +548 -47
- package/dist/esm/export.js +2 -0
- package/dist/esm/frameworks/nestjs.js +35 -0
- package/dist/esm/index.js +39 -3
- package/dist/esm/modules/express.js +60 -0
- package/dist/esm/modules/nestjs.js +47 -0
- package/dist/esm/uploader.js +3 -1
- package/dist/types/arch-detect.d.ts +9 -0
- package/dist/types/export.d.ts +2 -0
- package/dist/types/frameworks/nestjs.d.ts +10 -0
- package/dist/types/index.d.ts +11 -1
- package/dist/types/modules/express.d.ts +22 -0
- package/dist/types/modules/nestjs.d.ts +6 -0
- package/dist/types/uploader.d.ts +7 -0
- package/package.json +3 -2
package/dist/cjs/export.js
CHANGED
|
@@ -92,6 +92,8 @@ function enrichRoute(route, index) {
|
|
|
92
92
|
routeKey,
|
|
93
93
|
tag,
|
|
94
94
|
tags: route.tags?.length ? route.tags : [tag],
|
|
95
|
+
module: route.module,
|
|
96
|
+
service: route.service,
|
|
95
97
|
summary: route.summary || generateSummary(route.method, route.path),
|
|
96
98
|
description: route.description || generateDescription(route.method, route.path, tag),
|
|
97
99
|
authRequired,
|
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/cjs/uploader.js
CHANGED
|
@@ -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.
|
|
53
|
+
'User-Agent': 'apiforge-analyzer/1.3.0',
|
|
52
54
|
},
|
|
53
55
|
timeout: 30000,
|
|
54
56
|
});
|
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
// services/, apps/, or microservices/ dir containing 2+ independently-built
|
|
45
|
+
// subdirs (own Dockerfile and/or package.json) — the common Docker-Compose
|
|
46
|
+
// microservices layout, where each service lives one level deeper than the
|
|
47
|
+
// sibling-package check above looks.
|
|
48
|
+
for (const parentDir of ['services', 'apps', 'microservices']) {
|
|
49
|
+
const parentPath = join(rootDir, parentDir);
|
|
50
|
+
if (!existsSync(parentPath))
|
|
51
|
+
continue;
|
|
52
|
+
try {
|
|
53
|
+
const subdirs = readdirSync(parentPath, { withFileTypes: true }).filter(d => d.isDirectory());
|
|
54
|
+
const ownedServices = subdirs.filter(d => existsSync(join(parentPath, d.name, 'Dockerfile')) ||
|
|
55
|
+
existsSync(join(parentPath, d.name, 'package.json')));
|
|
56
|
+
if (ownedServices.length >= 2) {
|
|
57
|
+
microScore += 45;
|
|
58
|
+
ownedServices.forEach(d => servicePaths.push(`./${parentDir}/${d.name}`));
|
|
59
|
+
evidence.push(`${parentDir}/ has ${ownedServices.length} independently-built services: ${ownedServices.map(d => d.name).join(', ')}`);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
}
|
|
65
|
+
// ── Modular Monolithic signals ────────────────────────────────────────────────
|
|
66
|
+
// NestJS module files anywhere in src/
|
|
67
|
+
const srcDir = join(rootDir, 'src');
|
|
68
|
+
if (existsSync(srcDir)) {
|
|
69
|
+
const nestModules = findFilesRecursive(srcDir, '.module.ts');
|
|
70
|
+
if (nestModules.length > 0) {
|
|
71
|
+
modularScore += 40;
|
|
72
|
+
nestModules.forEach(f => {
|
|
73
|
+
const name = basename(f).replace('.module.ts', '');
|
|
74
|
+
if (!moduleNames.includes(name))
|
|
75
|
+
moduleNames.push(name);
|
|
76
|
+
});
|
|
77
|
+
evidence.push(`NestJS @Module files: ${nestModules.map(f => basename(f)).join(', ')}`);
|
|
78
|
+
}
|
|
79
|
+
// src/modules/, src/features/, or src/domains/
|
|
80
|
+
for (const dir of ['modules', 'features', 'domains']) {
|
|
81
|
+
if (existsSync(join(srcDir, dir))) {
|
|
82
|
+
modularScore += 35;
|
|
83
|
+
evidence.push(`Feature-based directory: src/${dir}/`);
|
|
84
|
+
const subdirs = readdirSync(join(srcDir, dir), { withFileTypes: true })
|
|
85
|
+
.filter(d => d.isDirectory())
|
|
86
|
+
.map(d => d.name);
|
|
87
|
+
subdirs.forEach(name => { if (!moduleNames.includes(name))
|
|
88
|
+
moduleNames.push(name); });
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// nest-cli.json at root
|
|
94
|
+
if (existsSync(join(rootDir, 'nest-cli.json'))) {
|
|
95
|
+
modularScore += 20;
|
|
96
|
+
evidence.push('nest-cli.json found');
|
|
97
|
+
}
|
|
98
|
+
// Classify
|
|
99
|
+
let type = 'monolithic';
|
|
100
|
+
let confidence = 0.6;
|
|
101
|
+
if (microScore >= 40) {
|
|
102
|
+
type = 'microservices';
|
|
103
|
+
confidence = Math.min(0.5 + microScore / 100, 0.98);
|
|
104
|
+
}
|
|
105
|
+
else if (modularScore >= 35) {
|
|
106
|
+
type = 'modular-monolithic';
|
|
107
|
+
confidence = Math.min(0.5 + modularScore / 100, 0.95);
|
|
108
|
+
}
|
|
109
|
+
if (evidence.length === 0) {
|
|
110
|
+
evidence.push('Single package.json, flat route structure');
|
|
111
|
+
}
|
|
112
|
+
return { type, confidence, evidence, moduleNames, servicePaths };
|
|
113
|
+
}
|
|
114
|
+
function findSiblingPackages(rootDir) {
|
|
115
|
+
const results = [];
|
|
116
|
+
try {
|
|
117
|
+
const entries = readdirSync(rootDir, { withFileTypes: true });
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
if (!entry.isDirectory())
|
|
120
|
+
continue;
|
|
121
|
+
if (['node_modules', '.git', 'dist', '.next', 'build'].includes(entry.name))
|
|
122
|
+
continue;
|
|
123
|
+
const pkgPath = join(rootDir, entry.name, 'package.json');
|
|
124
|
+
if (existsSync(pkgPath)) {
|
|
125
|
+
results.push(`./${entry.name}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch { }
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
132
|
+
function findFilesRecursive(dir, suffix, maxDepth = 6, depth = 0) {
|
|
133
|
+
if (depth > maxDepth)
|
|
134
|
+
return [];
|
|
135
|
+
const results = [];
|
|
136
|
+
try {
|
|
137
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
if (entry.name === 'node_modules' || entry.name === 'dist')
|
|
140
|
+
continue;
|
|
141
|
+
if (entry.isDirectory()) {
|
|
142
|
+
results.push(...findFilesRecursive(join(dir, entry.name), suffix, maxDepth, depth + 1));
|
|
143
|
+
}
|
|
144
|
+
else if (entry.isFile() && entry.name.endsWith(suffix)) {
|
|
145
|
+
results.push(join(dir, entry.name));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
return results;
|
|
151
|
+
}
|