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.
package/README.md CHANGED
@@ -1,279 +1,248 @@
1
- # APIForge Analyzer
1
+ # apiforge-analyzer
2
2
 
3
- Universal API documentation generator for **Express**, **Next.js**, **Vite**, **React Router**, and **Node.js**.
3
+ CLI and library for auto-generating API documentation from live Express, NestJS, Next.js, Vite, and React Router apps.
4
4
 
5
- Supports both **ESM** (`import`) and **CommonJS** (`require`).
5
+ Routes are extracted at runtime by walking the framework's internal route stack — no static analysis, no decorators, no changes to your production code.
6
6
 
7
- ## 📦 Installation
7
+ ## CLI usage
8
8
 
9
9
  ```bash
10
- npm install apiforge-analyzer
10
+ npx apiforge
11
+ ```
12
+
13
+ Run this from your project root. The CLI will:
14
+
15
+ 1. Detect your architecture (monolithic / modular-monolithic / microservices)
16
+ 2. Find your server entry file (or ask)
17
+ 3. Inject a temporary analysis snippet
18
+ 4. Start your server with the command you choose
19
+ 5. Collect all registered routes
20
+ 6. Upload to your APIForge dashboard for review
21
+ 7. Remove the injected code
22
+
23
+ For **microservices**, if an `apiforge.config.json` exists the CLI iterates every service sequentially, collects routes from each, then uploads everything in one batch. If no config file exists, the CLI scans your `services/` directory, shows what it found, and writes the config file for you — so the next run is fully automatic.
24
+
25
+ ```json
26
+ // apiforge.config.json — auto-created on first run, commit this file
27
+ {
28
+ "projectName": "My Backend",
29
+ "architectureType": "microservices",
30
+ "services": [
31
+ { "name": "auth-service", "dir": "./services/auth-service" },
32
+ { "name": "user-service", "dir": "./services/user-service" },
33
+ { "name": "order-service", "dir": "./services/order-service" }
34
+ ]
35
+ }
36
+ ```
37
+
38
+ Each service entry can also include `"startCommand"` to override the default (`node <entryFile>`):
39
+
40
+ ```json
41
+ { "name": "auth-service", "dir": "./services/auth", "startCommand": "npx tsx services/auth/src/index.ts" }
11
42
  ```
12
43
 
13
- ## 🚀 Quick Start
44
+ ## Programmatic API
45
+
46
+ Install:
47
+
48
+ ```bash
49
+ npm install apiforge-analyzer
50
+ ```
14
51
 
15
- ### Express (CommonJS)
52
+ ### Express
16
53
 
17
- ```javascript
54
+ ```js
18
55
  const express = require('express')
19
56
  const { analyze } = require('apiforge-analyzer')
20
57
 
21
58
  const app = express()
59
+ app.use(express.json())
22
60
 
23
- app.get('/api/users', (req, res) => res.json({ users: [] }))
24
- app.post('/api/users', (req, res) => res.json({ created: true }))
61
+ app.get('/api/users', getUsers)
62
+ app.post('/api/users', createUser)
63
+ app.get('/api/users/:id', getUserById)
25
64
 
65
+ // Call after all routes are registered
26
66
  analyze(app, {
27
67
  projectName: 'My API',
28
- apiKey: process.env.APIFORGE_KEY,
29
- upload: true
68
+ apiKey: process.env.APIFORGE_API_KEY,
69
+ baseUrl: 'https://api.myapp.com',
70
+ upload: true,
30
71
  })
31
72
 
32
73
  app.listen(3000)
33
74
  ```
34
75
 
35
- ### Express (ESM)
76
+ ### NestJS
36
77
 
37
- ```javascript
38
- import express from 'express'
78
+ ```ts
39
79
  import { analyze } from 'apiforge-analyzer'
40
80
 
41
- const app = express()
42
-
43
- app.get('/api/users', (req, res) => res.json({ users: [] }))
81
+ const app = await NestFactory.create(AppModule)
82
+ await app.init()
44
83
 
45
- await analyze(app, {
46
- projectName: 'My API',
47
- apiKey: process.env.APIFORGE_KEY
84
+ await analyze(app.getHttpServer(), {
85
+ projectName: 'My NestJS API',
86
+ apiKey: process.env.APIFORGE_API_KEY,
87
+ upload: true,
48
88
  })
49
89
 
50
- app.listen(3000)
90
+ await app.listen(3000)
91
+ ```
92
+
93
+ ### Microservices — per-service
94
+
95
+ When running each service independently, pass `service` to tag routes with the service name:
96
+
97
+ ```js
98
+ const { analyze } = require('apiforge-analyzer')
99
+
100
+ analyze(app, {
101
+ projectName: 'My Platform',
102
+ apiKey: process.env.APIFORGE_API_KEY,
103
+ architectureType: 'microservices',
104
+ service: 'auth-service',
105
+ upload: true,
106
+ })
51
107
  ```
52
108
 
53
109
  ### Next.js Pages Router
54
110
 
55
- ```javascript
56
- // pages/api/analyze.ts
111
+ ```ts
57
112
  import { analyzeNextPages } from 'apiforge-analyzer'
58
113
 
59
- export default async function handler(req, res) {
60
- await analyzeNextPages({
61
- projectName: 'My Next.js API',
62
- apiKey: process.env.APIFORGE_KEY,
63
- upload: true
64
- })
65
-
66
- res.json({ success: true })
67
- }
114
+ await analyzeNextPages({
115
+ projectName: 'My Next.js API',
116
+ apiKey: process.env.APIFORGE_API_KEY,
117
+ nextDir: 'pages/api',
118
+ upload: true,
119
+ })
68
120
  ```
69
121
 
70
122
  ### Next.js App Router
71
123
 
72
- ```javascript
73
- // app/api/analyze/route.ts
124
+ ```ts
74
125
  import { analyzeNextApp } from 'apiforge-analyzer'
75
126
 
76
- export async function GET() {
77
- await analyzeNextApp({
78
- projectName: 'My Next.js API',
79
- apiKey: process.env.APIFORGE_KEY,
80
- upload: true
81
- })
82
-
83
- return Response.json({ success: true })
84
- }
127
+ await analyzeNextApp({
128
+ projectName: 'My Next.js API',
129
+ apiKey: process.env.APIFORGE_API_KEY,
130
+ nextDir: 'app',
131
+ upload: true,
132
+ })
85
133
  ```
86
134
 
87
135
  ### Vite
88
136
 
89
- ```javascript
90
- // vite-analyze.js
137
+ ```ts
91
138
  import { analyzeVite } from 'apiforge-analyzer'
92
139
 
93
- // Analyzes proxy routes in vite.config.js
94
140
  await analyzeVite({
95
- projectName: 'My Vite App',
96
- apiKey: process.env.APIFORGE_KEY,
97
- upload: true
141
+ projectName: 'My App',
142
+ apiKey: process.env.APIFORGE_API_KEY,
143
+ nextDir: 'vite.config.ts',
144
+ upload: true,
98
145
  })
99
146
  ```
100
147
 
101
- **Example Vite Config:**
102
-
103
- ```javascript
104
- // vite.config.js
105
- export default {
106
- server: {
107
- proxy: {
108
- '/api': 'http://localhost:3000',
109
- '/auth': 'http://localhost:3001'
110
- }
111
- }
112
- }
113
- ```
114
-
115
148
  ### React Router
116
149
 
117
- ```javascript
118
- // analyze-routes.js
150
+ ```ts
119
151
  import { analyzeReactRouter } from 'apiforge-analyzer'
120
152
 
121
- // Analyzes routes from src/routes.tsx
122
153
  await analyzeReactRouter({
123
- projectName: 'My React App',
124
- apiKey: process.env.APIFORGE_KEY,
125
- upload: true
154
+ projectName: 'My App',
155
+ apiKey: process.env.APIFORGE_API_KEY,
156
+ nextDir: 'src/routes.tsx',
157
+ upload: true,
126
158
  })
127
159
  ```
128
160
 
129
- **Example Routes File:**
161
+ ## Options
130
162
 
131
- ```javascript
132
- // src/routes.tsx
133
- import { createBrowserRouter } from 'react-router-dom'
134
-
135
- export const router = createBrowserRouter([
136
- { path: '/', element: <Home /> },
137
- { path: '/users/:id', element: <UserDetail /> },
138
- { path: '/about', element: <About /> }
139
- ])
140
- ```
141
-
142
- ## ⚙️ Configuration
143
-
144
- ### Options
145
-
146
- ```typescript
163
+ ```ts
147
164
  interface AnalyzerOptions {
148
- // Required
149
- projectName: string // Name of your project
150
-
151
- // Optional
152
- baseUrl?: string // Base URL of your API
153
- apiKey?: string // APIForge API key
154
- apiForgeUrl?: string // APIForge server URL (default: http://localhost:3000)
155
- upload?: boolean // Upload to APIForge (default: true if apiKey provided)
156
- saveJson?: boolean // Save routes to JSON file
157
- jsonPath?: string // Path for JSON file (default: api-export.json)
158
- framework?: string // Force framework detection
159
- nextDir?: string // Next.js directory or config file path
160
- skipInProduction?: boolean // Skip in production (default: true)
161
- }
162
- ```
163
-
164
- ### Environment Variables
165
-
166
- ```bash
167
- APIFORGE_KEY=your-api-key
168
- APIFORGE_URL=http://localhost:3000 # or https://apiforge.dev
169
- NODE_ENV=development # Analysis skipped if production
170
- ```
165
+ projectName: string // required
171
166
 
172
- ## 📚 Framework Support
167
+ apiKey?: string // APIForge API key — also read from APIFORGE_API_KEY env var
168
+ baseUrl?: string // base URL shown in generated docs (e.g. https://api.myapp.com)
169
+ apiForgeUrl?: string // override backend URL (default: https://apiforgeapi.brainfogagency.com)
173
170
 
174
- | Framework | Status | Import |
175
- |-----------|--------|--------|
176
- | Express | ✅ Full | `analyzeExpress` or `analyze` |
177
- | Next.js Pages | ✅ Full | `analyzeNextPages` |
178
- | Next.js App Router | ✅ Full | `analyzeNextApp` |
179
- | Vite | ✅ Full | `analyzeVite` |
180
- | React Router | ✅ Full | `analyzeReactRouter` |
171
+ upload?: boolean // upload to dashboard (default: true when apiKey is set)
172
+ saveJson?: boolean // write routes to a local JSON file
173
+ jsonPath?: string // path for the JSON file (default: api-export.json)
181
174
 
182
- ## 📖 Examples
175
+ architectureType?: 'monolithic' | 'modular-monolithic' | 'microservices'
176
+ service?: string // service name for microservices mode — stamped on every route
183
177
 
184
- ### Export to JSON Only
178
+ framework?: string // skip auto-detection and force a framework
179
+ nextDir?: string // directory or config file path for Next.js / Vite / React Router
185
180
 
186
- ```javascript
187
- import { analyze } from 'apiforge-analyzer'
188
-
189
- await analyze(app, {
190
- projectName: 'My API',
191
- saveJson: true,
192
- jsonPath: './docs/api.json',
193
- upload: false // Don't upload
194
- })
195
- ```
196
-
197
- ### Auto-Detection
198
-
199
- ```javascript
200
- // Framework is auto-detected
201
- import { analyze } from 'apiforge-analyzer'
202
-
203
- await analyze(app, { projectName: 'My API' })
204
- ```
205
-
206
- ### Custom Config Paths
207
-
208
- ```javascript
209
- // Vite with custom config
210
- await analyzeVite({
211
- projectName: 'My App',
212
- nextDir: 'vite.config.ts' // Custom path
213
- })
214
-
215
- // React Router with custom routes file
216
- await analyzeReactRouter({
217
- projectName: 'My App',
218
- nextDir: 'src/app/routes.tsx' // Custom path
219
- })
181
+ skipInProduction?: boolean // skip when NODE_ENV=production (default: true)
182
+ runOnce?: boolean // debounce — skip if analyzed within the last 30 s (default: true)
183
+ }
220
184
  ```
221
185
 
222
- ### Skip in Production
223
-
224
- ```javascript
225
- // Automatically skips if NODE_ENV=production
226
- await analyze(app, {
227
- projectName: 'My API',
228
- skipInProduction: true // Default
229
- })
186
+ ## Architecture detection
187
+
188
+ The CLI scores structural signals to pick the right architecture automatically:
189
+
190
+ | Signal | Architecture |
191
+ |---|---|
192
+ | `apiforge.config.json` with `services[]` | microservices |
193
+ | `docker-compose.yml` with 3+ node services | microservices |
194
+ | Multiple `package.json` files in sibling dirs | microservices |
195
+ | `src/modules/` or `src/features/` directory | modular-monolithic |
196
+ | `*.module.ts` files (NestJS `@Module`) | modular-monolithic |
197
+ | None of the above | monolithic |
198
+
199
+ For modular-monolithic projects, routes are automatically grouped by module using path-prefix matching — `/api/users/*` gets tagged as the `users` module.
200
+
201
+ ## Output format
202
+
203
+ Each collected route:
204
+
205
+ ```ts
206
+ {
207
+ method: 'GET',
208
+ path: '/api/users/:id',
209
+ tag: 'users',
210
+ operationId: 'get-api-users-id',
211
+ summary: 'Get api users',
212
+ middleware: ['authenticate'],
213
+ authRequired: true,
214
+ pathParams: ['id'],
215
+ queryParams: [],
216
+ requestBodyFields: [],
217
+ responseStatuses: [200, 404],
218
+ framework: 'express',
219
+ moduleName?: 'users', // modular-monolithic only
220
+ serviceName?: 'auth-service' // microservices only
221
+ }
230
222
  ```
231
223
 
232
- ## 🔑 Getting an API Key
233
-
234
- 1. Sign up at [APIForge](https://apiforge.dev)
235
- 2. Go to Dashboard → API Keys
236
- 3. Generate a new key
237
- 4. Set `APIFORGE_KEY=your-key` in `.env`
238
-
239
- ## 🛠️ Module Support
224
+ ## ESM and CommonJS
240
225
 
241
- This package supports both **ESM** and **CommonJS**:
242
-
243
- ```javascript
226
+ ```js
244
227
  // ESM
245
228
  import { analyze } from 'apiforge-analyzer'
246
229
 
247
230
  // CommonJS
248
231
  const { analyze } = require('apiforge-analyzer')
249
-
250
- // Default import (CommonJS)
251
- const apiforge = require('apiforge-analyzer').default
252
232
  ```
253
233
 
254
- ## 📝 What Gets Analyzed
255
-
256
- - ✅ Route paths
257
- - ✅ HTTP methods (GET, POST, PUT, DELETE, etc.)
258
- - ✅ Path parameters (`:id`, `:userId`)
259
- - ✅ Middleware names
260
- - ✅ Route tags/groups
261
- - ✅ Response codes
262
- - ✅ Proxy configurations (Vite)
263
- - ✅ Frontend routes (React Router)
264
-
265
- ## 🧪 Testing Locally
266
-
267
- See [NPM_LINK_TEST.md](NPM_LINK_TEST.md) for detailed testing instructions using `npm link`.
268
-
269
- ## 🤝 Contributing
234
+ ## Environment variables
270
235
 
271
- Pull requests welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md).
236
+ | Variable | Description |
237
+ |---|---|
238
+ | `APIFORGE_API_KEY` | API key — loaded automatically, no dotenv required |
239
+ | `APIFORGE_URL` | Override backend URL |
240
+ | `NODE_ENV` | Set to `production` to disable analysis |
272
241
 
273
- ## 📄 License
242
+ ## Get an API key
274
243
 
275
- MIT © APIForge
244
+ [apiforge.brainfogagency.com/dashboard](https://apiforge.brainfogagency.com/dashboard)
276
245
 
277
- ---
246
+ ## License
278
247
 
279
- **Made with ❤️ by the APIForge team**
248
+ MIT
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectArchitectureType = detectArchitectureType;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ function detectArchitectureType(rootDir) {
7
+ let microScore = 0;
8
+ let modularScore = 0;
9
+ const evidence = [];
10
+ const servicePaths = [];
11
+ const moduleNames = [];
12
+ // ── Microservices signals ────────────────────────────────────────────────────
13
+ // Definitive: config file lists services[]
14
+ const configPaths = [(0, path_1.join)(rootDir, 'apiforge.config.ts'), (0, path_1.join)(rootDir, '.apiforge.json')];
15
+ for (const configPath of configPaths) {
16
+ if ((0, fs_1.existsSync)(configPath)) {
17
+ try {
18
+ const raw = (0, fs_1.readFileSync)(configPath, 'utf-8');
19
+ if (raw.includes('"services"') || raw.includes("'services'") || raw.includes('services:')) {
20
+ microScore += 50;
21
+ evidence.push(`Config file with services[]: ${(0, path_1.basename)(configPath)}`);
22
+ }
23
+ }
24
+ catch { }
25
+ }
26
+ }
27
+ // docker-compose.yml with multiple node service entries
28
+ const composePath = (0, path_1.join)(rootDir, 'docker-compose.yml');
29
+ if ((0, fs_1.existsSync)(composePath)) {
30
+ try {
31
+ const compose = (0, fs_1.readFileSync)(composePath, 'utf-8');
32
+ const serviceMatches = compose.match(/^\s{2}\w[\w-]+:/gm) || [];
33
+ if (serviceMatches.length >= 3) {
34
+ microScore += 35;
35
+ evidence.push(`docker-compose.yml with ${serviceMatches.length} services`);
36
+ }
37
+ }
38
+ catch { }
39
+ }
40
+ // Multiple sibling package.json at depth 1
41
+ const siblingPackages = findSiblingPackages(rootDir);
42
+ if (siblingPackages.length >= 2) {
43
+ microScore += 40;
44
+ siblingPackages.forEach(p => servicePaths.push(p));
45
+ evidence.push(`${siblingPackages.length} sibling package.json files (${siblingPackages.join(', ')})`);
46
+ }
47
+ // ── Modular Monolithic signals ────────────────────────────────────────────────
48
+ // NestJS module files anywhere in src/
49
+ const srcDir = (0, path_1.join)(rootDir, 'src');
50
+ if ((0, fs_1.existsSync)(srcDir)) {
51
+ const nestModules = findFilesRecursive(srcDir, '.module.ts');
52
+ if (nestModules.length > 0) {
53
+ modularScore += 40;
54
+ nestModules.forEach(f => {
55
+ const name = (0, path_1.basename)(f).replace('.module.ts', '');
56
+ if (!moduleNames.includes(name))
57
+ moduleNames.push(name);
58
+ });
59
+ evidence.push(`NestJS @Module files: ${nestModules.map(f => (0, path_1.basename)(f)).join(', ')}`);
60
+ }
61
+ // src/modules/, src/features/, or src/domains/
62
+ for (const dir of ['modules', 'features', 'domains']) {
63
+ if ((0, fs_1.existsSync)((0, path_1.join)(srcDir, dir))) {
64
+ modularScore += 35;
65
+ evidence.push(`Feature-based directory: src/${dir}/`);
66
+ const subdirs = (0, fs_1.readdirSync)((0, path_1.join)(srcDir, dir), { withFileTypes: true })
67
+ .filter(d => d.isDirectory())
68
+ .map(d => d.name);
69
+ subdirs.forEach(name => { if (!moduleNames.includes(name))
70
+ moduleNames.push(name); });
71
+ break;
72
+ }
73
+ }
74
+ }
75
+ // nest-cli.json at root
76
+ if ((0, fs_1.existsSync)((0, path_1.join)(rootDir, 'nest-cli.json'))) {
77
+ modularScore += 20;
78
+ evidence.push('nest-cli.json found');
79
+ }
80
+ // Classify
81
+ let type = 'monolithic';
82
+ let confidence = 0.6;
83
+ if (microScore >= 40) {
84
+ type = 'microservices';
85
+ confidence = Math.min(0.5 + microScore / 100, 0.98);
86
+ }
87
+ else if (modularScore >= 35) {
88
+ type = 'modular-monolithic';
89
+ confidence = Math.min(0.5 + modularScore / 100, 0.95);
90
+ }
91
+ if (evidence.length === 0) {
92
+ evidence.push('Single package.json, flat route structure');
93
+ }
94
+ return { type, confidence, evidence, moduleNames, servicePaths };
95
+ }
96
+ function findSiblingPackages(rootDir) {
97
+ const results = [];
98
+ try {
99
+ const entries = (0, fs_1.readdirSync)(rootDir, { withFileTypes: true });
100
+ for (const entry of entries) {
101
+ if (!entry.isDirectory())
102
+ continue;
103
+ if (['node_modules', '.git', 'dist', '.next', 'build'].includes(entry.name))
104
+ continue;
105
+ const pkgPath = (0, path_1.join)(rootDir, entry.name, 'package.json');
106
+ if ((0, fs_1.existsSync)(pkgPath)) {
107
+ results.push(`./${entry.name}`);
108
+ }
109
+ }
110
+ }
111
+ catch { }
112
+ return results;
113
+ }
114
+ function findFilesRecursive(dir, suffix, maxDepth = 6, depth = 0) {
115
+ if (depth > maxDepth)
116
+ return [];
117
+ const results = [];
118
+ try {
119
+ const entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
120
+ for (const entry of entries) {
121
+ if (entry.name === 'node_modules' || entry.name === 'dist')
122
+ continue;
123
+ if (entry.isDirectory()) {
124
+ results.push(...findFilesRecursive((0, path_1.join)(dir, entry.name), suffix, maxDepth, depth + 1));
125
+ }
126
+ else if (entry.isFile() && entry.name.endsWith(suffix)) {
127
+ results.push((0, path_1.join)(dir, entry.name));
128
+ }
129
+ }
130
+ }
131
+ catch { }
132
+ return results;
133
+ }