apiforge-analyzer 1.3.6 → 1.3.8
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 +66 -2
- package/dist/cjs/cli.js +762 -0
- package/dist/cjs/detect.js +48 -0
- package/dist/cjs/frameworks/python-introspect.js +555 -0
- package/dist/cjs/frameworks/python.js +209 -0
- package/dist/cjs/index.js +16 -1
- package/dist/esm/cli.js +764 -2
- package/dist/esm/detect.js +47 -1
- package/dist/esm/frameworks/python-introspect.js +552 -0
- package/dist/esm/frameworks/python.js +171 -0
- package/dist/esm/index.js +15 -1
- package/dist/types/detect.d.ts +12 -1
- package/dist/types/frameworks/python-introspect.d.ts +18 -0
- package/dist/types/frameworks/python.d.ts +52 -0
- package/dist/types/index.d.ts +11 -1
- package/package.json +1 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* FastAPI / Flask / Django (+ DRF) analyzer
|
|
4
|
+
*
|
|
5
|
+
* Python apps can't be handed to analyze() as a JS object, so this runs the
|
|
6
|
+
* user's own interpreter (their venv, so their dependencies resolve), imports
|
|
7
|
+
* the app, and reads the framework's live route table — see
|
|
8
|
+
* python-introspect.ts. Nothing is injected into the user's code and no server
|
|
9
|
+
* is started. Returns the same Route[] as every other analyzer; enrichment and
|
|
10
|
+
* upload happen in index.ts / uploader.ts exactly as for Express or Fastify.
|
|
11
|
+
*
|
|
12
|
+
* The `npx apiforge` CLI instead works like its Node flow: it injects a snippet
|
|
13
|
+
* into the file returned in PythonAppCandidate.file and runs the user's own
|
|
14
|
+
* start command (see pythonFlow in cli.ts).
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.analyzePython = analyzePython;
|
|
51
|
+
exports.findPythonInterpreter = findPythonInterpreter;
|
|
52
|
+
exports.findPythonApps = findPythonApps;
|
|
53
|
+
const fs = __importStar(require("fs"));
|
|
54
|
+
const path = __importStar(require("path"));
|
|
55
|
+
const child_process_1 = require("child_process");
|
|
56
|
+
const python_introspect_js_1 = require("./python-introspect.js");
|
|
57
|
+
const SKIP_DIRS = new Set([
|
|
58
|
+
'.git', '.venv', 'venv', 'env', '.env', 'node_modules', '__pycache__', 'site-packages',
|
|
59
|
+
'.tox', '.mypy_cache', '.pytest_cache', 'dist', 'build', 'migrations',
|
|
60
|
+
]);
|
|
61
|
+
/**
|
|
62
|
+
* Analyze a Python web app's routes
|
|
63
|
+
*/
|
|
64
|
+
async function analyzePython(options) {
|
|
65
|
+
let appDir = options.appDir || process.cwd();
|
|
66
|
+
let { app, factory, djangoSettings } = options;
|
|
67
|
+
// Fill in whatever the caller left out from the project layout.
|
|
68
|
+
if (options.framework === 'django' ? !app && !djangoSettings && !process.env.DJANGO_SETTINGS_MODULE : !app) {
|
|
69
|
+
const [candidate] = findPythonApps(options.framework, appDir);
|
|
70
|
+
if (!candidate) {
|
|
71
|
+
throw new Error(options.framework === 'django'
|
|
72
|
+
? 'Django settings not found — pass djangoSettings (e.g. "mysite.settings")'
|
|
73
|
+
: `${options.framework} app not found — pass pythonApp (e.g. "main:app")`);
|
|
74
|
+
}
|
|
75
|
+
;
|
|
76
|
+
({ app, factory, djangoSettings, appDir } = candidate);
|
|
77
|
+
}
|
|
78
|
+
const pythonPath = options.pythonPath || findPythonInterpreter(appDir);
|
|
79
|
+
const stdout = await runPython(pythonPath, appDir, {
|
|
80
|
+
framework: options.framework, app, factory, djangoSettings, appDir,
|
|
81
|
+
}, options.timeoutMs ?? 60000);
|
|
82
|
+
const line = stdout.split(/\r?\n/).find(l => l.startsWith(python_introspect_js_1.ROUTES_MARKER));
|
|
83
|
+
if (!line)
|
|
84
|
+
throw new Error('Python analyzer produced no output');
|
|
85
|
+
return JSON.parse(line.slice(python_introspect_js_1.ROUTES_MARKER.length));
|
|
86
|
+
}
|
|
87
|
+
function runPython(pythonPath, cwd, opts, timeoutMs) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const child = (0, child_process_1.spawn)(pythonPath, ['-'], {
|
|
90
|
+
cwd,
|
|
91
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
92
|
+
env: {
|
|
93
|
+
...process.env,
|
|
94
|
+
APIFORGE_PY_OPTIONS: JSON.stringify(opts),
|
|
95
|
+
PYTHONIOENCODING: 'utf-8',
|
|
96
|
+
// Don't leave __pycache__ folders behind in the user's project.
|
|
97
|
+
PYTHONDONTWRITEBYTECODE: '1',
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
let stdout = '';
|
|
101
|
+
let stderr = '';
|
|
102
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString('utf-8'); });
|
|
103
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf-8'); });
|
|
104
|
+
const timer = setTimeout(() => {
|
|
105
|
+
child.kill();
|
|
106
|
+
reject(new Error(`Python analyzer timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
107
|
+
}, timeoutMs);
|
|
108
|
+
child.on('error', (err) => {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
reject(new Error(err.code === 'ENOENT'
|
|
111
|
+
? `Python interpreter not found: "${pythonPath}" — activate your virtualenv or set APIFORGE_PYTHON`
|
|
112
|
+
: err.message));
|
|
113
|
+
});
|
|
114
|
+
child.on('close', code => {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
if (code === 0)
|
|
117
|
+
return resolve(stdout);
|
|
118
|
+
// The last traceback line is the actual error (ModuleNotFoundError: ..., etc.)
|
|
119
|
+
const lastLine = stderr.trim().split(/\r?\n/).pop() || `exited with code ${code}`;
|
|
120
|
+
reject(new Error(`Python analyzer failed: ${lastLine}`));
|
|
121
|
+
});
|
|
122
|
+
child.stdin.end(python_introspect_js_1.PYTHON_INTROSPECT_SCRIPT);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The interpreter that has the project's dependencies installed:
|
|
127
|
+
* APIFORGE_PYTHON → active virtualenv → a venv inside the project → system python.
|
|
128
|
+
*/
|
|
129
|
+
function findPythonInterpreter(projectDir = process.cwd()) {
|
|
130
|
+
if (process.env.APIFORGE_PYTHON)
|
|
131
|
+
return process.env.APIFORGE_PYTHON;
|
|
132
|
+
const inVenv = (venvDir) => process.platform === 'win32'
|
|
133
|
+
? path.join(venvDir, 'Scripts', 'python.exe')
|
|
134
|
+
: path.join(venvDir, 'bin', 'python');
|
|
135
|
+
const candidates = [
|
|
136
|
+
process.env.VIRTUAL_ENV,
|
|
137
|
+
...['.venv', 'venv', 'env'].map(dir => path.join(projectDir, dir)),
|
|
138
|
+
].filter(Boolean).map(dir => inVenv(dir));
|
|
139
|
+
return candidates.find(p => fs.existsSync(p)) || (process.platform === 'win32' ? 'python' : 'python3');
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Find where the app object (FastAPI/Flask) or settings module (Django) lives.
|
|
143
|
+
*/
|
|
144
|
+
function findPythonApps(framework, projectDir = process.cwd()) {
|
|
145
|
+
const files = listPythonFiles(projectDir, 3);
|
|
146
|
+
if (framework === 'django') {
|
|
147
|
+
const candidates = [];
|
|
148
|
+
for (const file of files.filter(f => path.basename(f) === 'manage.py')) {
|
|
149
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
150
|
+
const match = content.match(/DJANGO_SETTINGS_MODULE['"]\s*,\s*['"]([\w.]+)['"]/);
|
|
151
|
+
if (match) {
|
|
152
|
+
const appDir = path.dirname(file);
|
|
153
|
+
// The CLI injects into manage.py itself: every runserver goes through it, whereas
|
|
154
|
+
// wsgi.py is only loaded when settings.WSGI_APPLICATION points at it.
|
|
155
|
+
candidates.push({ label: `${match[1]} (${path.relative(projectDir, file) || 'manage.py'})`, djangoSettings: match[1], appDir, file });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return candidates;
|
|
159
|
+
}
|
|
160
|
+
const ctor = framework === 'fastapi' ? 'FastAPI' : 'Flask';
|
|
161
|
+
const instanceRe = new RegExp(`^(\\w+)\\s*(?::\\s*\\w+\\s*)?=\\s*(?:\\w+\\.)?${ctor}\\(`, 'gm');
|
|
162
|
+
const candidates = [];
|
|
163
|
+
for (const file of files) {
|
|
164
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
165
|
+
if (!content.includes(ctor))
|
|
166
|
+
continue;
|
|
167
|
+
// src/ layout: import from inside src/ so the app's own absolute imports resolve
|
|
168
|
+
const rel = path.relative(projectDir, file).split(path.sep);
|
|
169
|
+
const appDir = rel[0] === 'src' ? path.join(projectDir, 'src') : projectDir;
|
|
170
|
+
if (rel[0] === 'src')
|
|
171
|
+
rel.shift();
|
|
172
|
+
const module = rel.join('.').replace(/\.py$/, '').replace(/\.__init__$/, '');
|
|
173
|
+
for (const match of content.matchAll(instanceRe)) {
|
|
174
|
+
candidates.push({ label: `${module}:${match[1]}`, app: `${module}:${match[1]}`, appDir, file });
|
|
175
|
+
}
|
|
176
|
+
if (framework === 'flask' && !content.match(instanceRe)) {
|
|
177
|
+
const factory = content.match(/^def\s+(create_app|make_app)\s*\(/m);
|
|
178
|
+
if (factory)
|
|
179
|
+
candidates.push({ label: `${module}:${factory[1]}()`, app: `${module}:${factory[1]}`, factory: true, appDir, file });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Top-level entry files first: main.py / app.py beat a FastAPI() in some sub-module
|
|
183
|
+
return candidates.sort((a, b) => depth(a.app) - depth(b.app));
|
|
184
|
+
}
|
|
185
|
+
function depth(app) {
|
|
186
|
+
return (app || '').split(':')[0].split('.').length;
|
|
187
|
+
}
|
|
188
|
+
function listPythonFiles(dir, maxDepth, level = 0) {
|
|
189
|
+
let entries;
|
|
190
|
+
try {
|
|
191
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
const files = [];
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
const full = path.join(dir, entry.name);
|
|
199
|
+
if (entry.isDirectory()) {
|
|
200
|
+
if (level < maxDepth && !SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
201
|
+
files.push(...listPythonFiles(full, maxDepth, level + 1));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (entry.isFile() && entry.name.endsWith('.py')) {
|
|
205
|
+
files.push(full);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return files;
|
|
209
|
+
}
|
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.detectArchitectureType = exports.detectFramework = exports.uploadRoutes = exports.exportToJSON = exports.analyzeFastify = exports.analyzeNestJS = exports.analyzeReactRouter = exports.analyzeVite = exports.analyzeNextApp = exports.analyzeNextPages = exports.analyzeExpress = void 0;
|
|
6
|
+
exports.detectArchitectureType = exports.detectFramework = exports.uploadRoutes = exports.exportToJSON = exports.analyzePython = exports.analyzeFastify = 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");
|
|
@@ -21,6 +21,8 @@ const nestjs_js_1 = require("./frameworks/nestjs.js");
|
|
|
21
21
|
Object.defineProperty(exports, "analyzeNestJS", { enumerable: true, get: function () { return nestjs_js_1.analyzeNestJS; } });
|
|
22
22
|
const fastify_js_1 = require("./frameworks/fastify.js");
|
|
23
23
|
Object.defineProperty(exports, "analyzeFastify", { enumerable: true, get: function () { return fastify_js_1.analyzeFastify; } });
|
|
24
|
+
const python_js_1 = require("./frameworks/python.js");
|
|
25
|
+
Object.defineProperty(exports, "analyzePython", { enumerable: true, get: function () { return python_js_1.analyzePython; } });
|
|
24
26
|
const detect_js_1 = require("./detect.js");
|
|
25
27
|
const export_js_1 = require("./export.js");
|
|
26
28
|
const uploader_js_1 = require("./uploader.js");
|
|
@@ -178,6 +180,18 @@ async function analyze(app, options) {
|
|
|
178
180
|
case 'react-router':
|
|
179
181
|
routes = await (0, react_router_js_1.analyzeReactRouter)(options.nextDir || 'src/routes.tsx');
|
|
180
182
|
break;
|
|
183
|
+
case 'fastapi':
|
|
184
|
+
case 'flask':
|
|
185
|
+
case 'django':
|
|
186
|
+
routes = await (0, python_js_1.analyzePython)({
|
|
187
|
+
framework: framework,
|
|
188
|
+
app: options.pythonApp,
|
|
189
|
+
factory: options.pythonFactory,
|
|
190
|
+
pythonPath: options.pythonPath,
|
|
191
|
+
djangoSettings: options.djangoSettings,
|
|
192
|
+
appDir: options.nextDir,
|
|
193
|
+
});
|
|
194
|
+
break;
|
|
181
195
|
// No default: unsupported frameworks are rejected above, before this
|
|
182
196
|
// try block, so a thrown error here isn't silently swallowed into `[]`.
|
|
183
197
|
}
|
|
@@ -239,6 +253,7 @@ exports.default = {
|
|
|
239
253
|
analyzeVite: vite_js_1.analyzeVite,
|
|
240
254
|
analyzeReactRouter: react_router_js_1.analyzeReactRouter,
|
|
241
255
|
analyzeNestJS: nestjs_js_1.analyzeNestJS,
|
|
256
|
+
analyzePython: python_js_1.analyzePython,
|
|
242
257
|
exportToJSON: export_js_1.exportToJSON,
|
|
243
258
|
uploadRoutes: uploader_js_1.uploadRoutes,
|
|
244
259
|
detectFramework: detect_js_1.detectFramework,
|