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/cjs/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
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
|
+
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = require("path");
|
|
8
10
|
const express_js_1 = require("./frameworks/express.js");
|
|
9
11
|
Object.defineProperty(exports, "analyzeExpress", { enumerable: true, get: function () { return express_js_1.analyzeExpress; } });
|
|
10
12
|
const nextjs_pages_js_1 = require("./frameworks/nextjs-pages.js");
|
|
@@ -15,92 +17,185 @@ const vite_js_1 = require("./frameworks/vite.js");
|
|
|
15
17
|
Object.defineProperty(exports, "analyzeVite", { enumerable: true, get: function () { return vite_js_1.analyzeVite; } });
|
|
16
18
|
const react_router_js_1 = require("./frameworks/react-router.js");
|
|
17
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; } });
|
|
18
22
|
const detect_js_1 = require("./detect.js");
|
|
19
23
|
const export_js_1 = require("./export.js");
|
|
20
24
|
const uploader_js_1 = require("./uploader.js");
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
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");
|
|
28
|
+
// ─── Env file loader — no dotenv dependency ───────────────────────────────────
|
|
24
29
|
/**
|
|
25
|
-
*
|
|
30
|
+
* Read a single variable from a .env-style file.
|
|
31
|
+
* Plain Node/Express doesn't auto-load .env.local, so we do it ourselves.
|
|
32
|
+
* The package never requires dotenv or any other dep for this.
|
|
26
33
|
*/
|
|
34
|
+
function readEnvFile(varName) {
|
|
35
|
+
const candidates = [
|
|
36
|
+
'.env.local',
|
|
37
|
+
'.env.development.local',
|
|
38
|
+
'.env.development',
|
|
39
|
+
'.env',
|
|
40
|
+
];
|
|
41
|
+
for (const file of candidates) {
|
|
42
|
+
try {
|
|
43
|
+
const content = (0, fs_1.readFileSync)((0, path_1.join)(process.cwd(), file), 'utf-8');
|
|
44
|
+
for (const line of content.split('\n')) {
|
|
45
|
+
const trimmed = line.trim();
|
|
46
|
+
if (trimmed.startsWith('#') || !trimmed.includes('='))
|
|
47
|
+
continue;
|
|
48
|
+
const eqIdx = trimmed.indexOf('=');
|
|
49
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
50
|
+
if (key !== varName)
|
|
51
|
+
continue;
|
|
52
|
+
return trimmed.slice(eqIdx + 1).trim().replace(/^['"]|['"]$/g, '');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch { }
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
// ─── File-based debounce (survives nodemon restarts) ──────────────────────────
|
|
60
|
+
const DEBOUNCE_FILE = '.apiforge-last-run.json';
|
|
61
|
+
const DEBOUNCE_MS = 30000; // 30 s
|
|
62
|
+
function readLastRunMs() {
|
|
63
|
+
try {
|
|
64
|
+
const raw = (0, fs_1.readFileSync)((0, path_1.join)(process.cwd(), DEBOUNCE_FILE), 'utf-8');
|
|
65
|
+
return JSON.parse(raw).timestamp ?? 0;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function stampLastRun() {
|
|
72
|
+
try {
|
|
73
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(process.cwd(), DEBOUNCE_FILE), JSON.stringify({ timestamp: Date.now() }), 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
catch { }
|
|
76
|
+
}
|
|
77
|
+
// ─── Main analyze ─────────────────────────────────────────────────────────────
|
|
27
78
|
async function analyze(app, options) {
|
|
28
|
-
//
|
|
79
|
+
// Never run in production
|
|
29
80
|
if (options.skipInProduction !== false && process.env.NODE_ENV === 'production') {
|
|
30
|
-
console.log('⏭️ Skipping analysis in production');
|
|
31
81
|
return [];
|
|
32
82
|
}
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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) {
|
|
87
|
+
const elapsed = Date.now() - readLastRunMs();
|
|
88
|
+
if (elapsed < DEBOUNCE_MS) {
|
|
89
|
+
console.log(`⚡ APIForge: skipped (last run ${Math.round(elapsed / 1000)}s ago)`);
|
|
90
|
+
return [];
|
|
40
91
|
}
|
|
92
|
+
stampLastRun();
|
|
41
93
|
}
|
|
42
|
-
|
|
43
|
-
//
|
|
94
|
+
// ── Resolve API key ─────────────────────────────────────────────────────────
|
|
95
|
+
// Priority: explicit option → process.env → .env.local/.env file
|
|
96
|
+
// This means the package works even if the user's server doesn't load dotenv.
|
|
97
|
+
if (!options.apiKey) {
|
|
98
|
+
options.apiKey =
|
|
99
|
+
process.env.APIFORGE_API_KEY ||
|
|
100
|
+
readEnvFile('APIFORGE_API_KEY');
|
|
101
|
+
}
|
|
102
|
+
console.log(`🔍 APIForge: analyzing ${options.projectName}…`);
|
|
103
|
+
// Writing saveJson files into the watched dir triggers nodemon restart loops
|
|
44
104
|
if (options.upload !== false && options.apiKey) {
|
|
45
105
|
options.saveJson = false;
|
|
46
106
|
}
|
|
107
|
+
// ── Early failure: upload desired but no API key anywhere ──────────────────
|
|
108
|
+
if (options.upload !== false && !options.apiKey) {
|
|
109
|
+
const msg = 'APIFORGE_API_KEY not found in options, process.env, or .env.local';
|
|
110
|
+
console.error(`❌ APIForge: ${msg}`);
|
|
111
|
+
(0, uploader_js_1.writeStatusFile)('error', { error: msg });
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
47
114
|
let routes = [];
|
|
48
115
|
let framework = options.framework;
|
|
49
|
-
// Auto-detect framework if not specified
|
|
50
116
|
if (!framework) {
|
|
51
|
-
|
|
52
|
-
framework = detected.name;
|
|
117
|
+
framework = (0, detect_js_1.detectFramework)(app).name;
|
|
53
118
|
console.log(`📦 Detected framework: ${framework}`);
|
|
54
119
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
120
|
+
try {
|
|
121
|
+
switch (framework) {
|
|
122
|
+
case 'nestjs':
|
|
123
|
+
routes = await (0, nestjs_js_1.analyzeNestJS)(app, options.nextDir || process.cwd());
|
|
124
|
+
break;
|
|
125
|
+
case 'express':
|
|
126
|
+
if (!app) {
|
|
127
|
+
const msg = 'Express app instance required — pass your app object to analyze()';
|
|
128
|
+
console.error(`❌ ${msg}`);
|
|
129
|
+
(0, uploader_js_1.writeStatusFile)('error', { error: msg });
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
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
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
case 'nextjs-pages':
|
|
145
|
+
routes = await (0, nextjs_pages_js_1.analyzeNextPages)(options.nextDir || 'pages/api');
|
|
146
|
+
break;
|
|
147
|
+
case 'nextjs-app':
|
|
148
|
+
routes = await (0, nextjs_app_js_1.analyzeNextApp)(options.nextDir || 'app');
|
|
149
|
+
break;
|
|
150
|
+
case 'vite':
|
|
151
|
+
routes = await (0, vite_js_1.analyzeVite)(options.nextDir || 'vite.config.js');
|
|
152
|
+
break;
|
|
153
|
+
case 'react-router':
|
|
154
|
+
routes = await (0, react_router_js_1.analyzeReactRouter)(options.nextDir || 'src/routes.tsx');
|
|
155
|
+
break;
|
|
156
|
+
default:
|
|
157
|
+
const msg = `Framework "${framework}" not supported yet (express, nextjs-pages, nextjs-app, vite, react-router)`;
|
|
158
|
+
console.warn(`⚠️ APIForge: ${msg}`);
|
|
159
|
+
(0, uploader_js_1.writeStatusFile)('error', { error: msg });
|
|
60
160
|
return [];
|
|
61
|
-
|
|
62
|
-
routes = await (0, express_js_1.analyzeExpress)(app);
|
|
63
|
-
break;
|
|
64
|
-
case 'nextjs-pages':
|
|
65
|
-
routes = await (0, nextjs_pages_js_1.analyzeNextPages)(options.nextDir || 'pages/api');
|
|
66
|
-
break;
|
|
67
|
-
case 'nextjs-app':
|
|
68
|
-
routes = await (0, nextjs_app_js_1.analyzeNextApp)(options.nextDir || 'app');
|
|
69
|
-
break;
|
|
70
|
-
case 'vite':
|
|
71
|
-
routes = await (0, vite_js_1.analyzeVite)(options.nextDir || 'vite.config.js');
|
|
72
|
-
break;
|
|
73
|
-
case 'react-router':
|
|
74
|
-
routes = await (0, react_router_js_1.analyzeReactRouter)(options.nextDir || 'src/routes.tsx');
|
|
75
|
-
break;
|
|
76
|
-
case 'node-http':
|
|
77
|
-
case 'unknown':
|
|
78
|
-
default:
|
|
79
|
-
console.warn(`⚠️ Framework "${framework}" not supported yet`);
|
|
80
|
-
console.log('💡 Supported: express, nextjs-pages, nextjs-app, vite, react-router');
|
|
81
|
-
console.log('💡 Tip: Specify framework manually: { framework: "express" }');
|
|
82
|
-
return [];
|
|
161
|
+
}
|
|
83
162
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
routes
|
|
90
|
-
});
|
|
163
|
+
catch (err) {
|
|
164
|
+
const msg = err.message || 'Unknown error during route extraction';
|
|
165
|
+
console.error(`❌ APIForge analysis error: ${msg}`);
|
|
166
|
+
(0, uploader_js_1.writeStatusFile)('error', { error: msg });
|
|
167
|
+
return [];
|
|
91
168
|
}
|
|
92
|
-
|
|
169
|
+
if (routes.length === 0) {
|
|
170
|
+
const msg = 'No routes found — ensure routes are registered before analyze() is called';
|
|
171
|
+
console.warn(`⚠️ APIForge: ${msg}`);
|
|
172
|
+
(0, uploader_js_1.writeStatusFile)('error', { error: msg });
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
console.log(`✅ Found ${routes.length} routes`);
|
|
93
176
|
if (options.saveJson) {
|
|
94
|
-
|
|
95
|
-
|
|
177
|
+
await (0, export_js_1.exportToJSON)(routes, options.projectName, options.jsonPath || 'api-export.json', options.baseUrl);
|
|
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;
|
|
96
191
|
}
|
|
97
|
-
// Upload to APIForge
|
|
98
192
|
if (options.upload !== false && options.apiKey) {
|
|
99
193
|
await (0, uploader_js_1.uploadRoutes)(routes, {
|
|
100
194
|
projectName: options.projectName,
|
|
101
195
|
apiKey: options.apiKey,
|
|
102
196
|
baseUrl: options.baseUrl,
|
|
103
197
|
apiForgeUrl: options.apiForgeUrl,
|
|
198
|
+
architectureType: options.architectureType,
|
|
104
199
|
});
|
|
105
200
|
}
|
|
106
201
|
return routes;
|
|
@@ -111,7 +206,8 @@ var uploader_js_2 = require("./uploader.js");
|
|
|
111
206
|
Object.defineProperty(exports, "uploadRoutes", { enumerable: true, get: function () { return uploader_js_2.uploadRoutes; } });
|
|
112
207
|
var detect_js_2 = require("./detect.js");
|
|
113
208
|
Object.defineProperty(exports, "detectFramework", { enumerable: true, get: function () { return detect_js_2.detectFramework; } });
|
|
114
|
-
|
|
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; } });
|
|
115
211
|
exports.default = {
|
|
116
212
|
analyze,
|
|
117
213
|
analyzeExpress: express_js_1.analyzeExpress,
|
|
@@ -119,7 +215,9 @@ exports.default = {
|
|
|
119
215
|
analyzeNextApp: nextjs_app_js_1.analyzeNextApp,
|
|
120
216
|
analyzeVite: vite_js_1.analyzeVite,
|
|
121
217
|
analyzeReactRouter: react_router_js_1.analyzeReactRouter,
|
|
218
|
+
analyzeNestJS: nestjs_js_1.analyzeNestJS,
|
|
122
219
|
exportToJSON: export_js_1.exportToJSON,
|
|
123
220
|
uploadRoutes: uploader_js_1.uploadRoutes,
|
|
124
|
-
detectFramework: detect_js_1.detectFramework
|
|
221
|
+
detectFramework: detect_js_1.detectFramework,
|
|
222
|
+
detectArchitectureType: arch_detect_js_1.detectArchitectureType,
|
|
125
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
|
@@ -3,32 +3,72 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.writeStatusFile = writeStatusFile;
|
|
7
|
+
exports.removeStatusFile = removeStatusFile;
|
|
6
8
|
exports.uploadRoutes = uploadRoutes;
|
|
7
9
|
const axios_1 = __importDefault(require("axios"));
|
|
10
|
+
const fs_1 = require("fs");
|
|
11
|
+
const path_1 = require("path");
|
|
8
12
|
const export_js_1 = require("./export.js");
|
|
13
|
+
/** Status file written to the project root so the CLI can detect completion. */
|
|
14
|
+
const STATUS_FILE = '.apiforge-status.json';
|
|
15
|
+
function writeStatusFile(status, data) {
|
|
16
|
+
try {
|
|
17
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(process.cwd(), STATUS_FILE), JSON.stringify({ status, timestamp: Date.now(), ...data }), 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
}
|
|
21
|
+
function removeStatusFile() {
|
|
22
|
+
try {
|
|
23
|
+
const p = (0, path_1.join)(process.cwd(), STATUS_FILE);
|
|
24
|
+
if ((0, fs_1.existsSync)(p))
|
|
25
|
+
(0, fs_1.unlinkSync)(p);
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
}
|
|
9
29
|
/**
|
|
10
|
-
*
|
|
30
|
+
* Submit routes to the APIForge backend for user review.
|
|
31
|
+
* Returns the pendingImportId so the CLI can poll for approval.
|
|
11
32
|
*/
|
|
12
33
|
async function uploadRoutes(routes, options) {
|
|
13
|
-
|
|
34
|
+
// Production URL is the built-in default — no config needed.
|
|
35
|
+
// Override only for local testing via option or env var.
|
|
36
|
+
const backendUrl = (options.apiForgeUrl ||
|
|
37
|
+
process.env.APIFORGE_URL ||
|
|
38
|
+
'https://apiforgeapi.brainfogagency.com').replace(/\/$/, '');
|
|
14
39
|
const enrichedRoutes = (0, export_js_1.enrichRoutes)(routes);
|
|
40
|
+
const payload = {
|
|
41
|
+
projectName: options.projectName,
|
|
42
|
+
routes: enrichedRoutes,
|
|
43
|
+
baseUrl: options.baseUrl,
|
|
44
|
+
framework: enrichedRoutes[0]?.framework || 'unknown',
|
|
45
|
+
architectureType: options.architectureType || 'monolithic',
|
|
46
|
+
services: options.services || [],
|
|
47
|
+
};
|
|
15
48
|
try {
|
|
16
|
-
const response = await axios_1.default.post(`${
|
|
17
|
-
projectName: options.projectName,
|
|
18
|
-
routes: enrichedRoutes,
|
|
19
|
-
baseUrl: options.baseUrl,
|
|
20
|
-
framework: enrichedRoutes[0]?.framework || 'unknown',
|
|
21
|
-
}, {
|
|
49
|
+
const response = await axios_1.default.post(`${backendUrl}/api/cli/submit`, payload, {
|
|
22
50
|
headers: {
|
|
23
51
|
'Content-Type': 'application/json',
|
|
24
52
|
'X-API-Key': options.apiKey,
|
|
53
|
+
'User-Agent': 'apiforge-analyzer/1.3.0',
|
|
25
54
|
},
|
|
26
55
|
timeout: 30000,
|
|
27
56
|
});
|
|
28
|
-
|
|
57
|
+
const data = response.data?.data;
|
|
58
|
+
console.log(`✅ APIForge: ${data?.message || 'Routes submitted for review'}`);
|
|
59
|
+
writeStatusFile('submitted', {
|
|
60
|
+
pendingImportId: data.pendingImportId,
|
|
61
|
+
routeCount: data.routeCount,
|
|
62
|
+
});
|
|
63
|
+
return data;
|
|
29
64
|
}
|
|
30
65
|
catch (error) {
|
|
31
|
-
|
|
32
|
-
|
|
66
|
+
const msg = error.response?.data?.message ||
|
|
67
|
+
error.response?.data?.error ||
|
|
68
|
+
error.message ||
|
|
69
|
+
'Unknown error';
|
|
70
|
+
console.error(`❌ APIForge upload failed: ${msg}`);
|
|
71
|
+
writeStatusFile('error', { error: msg });
|
|
72
|
+
return null;
|
|
33
73
|
}
|
|
34
74
|
}
|
|
@@ -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
|
+
}
|