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,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FastAPI / Flask / Django (+ DRF) analyzer
|
|
3
|
+
*
|
|
4
|
+
* Python apps can't be handed to analyze() as a JS object, so this runs the
|
|
5
|
+
* user's own interpreter (their venv, so their dependencies resolve), imports
|
|
6
|
+
* the app, and reads the framework's live route table — see
|
|
7
|
+
* python-introspect.ts. Nothing is injected into the user's code and no server
|
|
8
|
+
* is started. Returns the same Route[] as every other analyzer; enrichment and
|
|
9
|
+
* upload happen in index.ts / uploader.ts exactly as for Express or Fastify.
|
|
10
|
+
*
|
|
11
|
+
* The `npx apiforge` CLI instead works like its Node flow: it injects a snippet
|
|
12
|
+
* into the file returned in PythonAppCandidate.file and runs the user's own
|
|
13
|
+
* start command (see pythonFlow in cli.ts).
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { spawn } from 'child_process';
|
|
18
|
+
import { PYTHON_INTROSPECT_SCRIPT, ROUTES_MARKER } from './python-introspect.js';
|
|
19
|
+
const SKIP_DIRS = new Set([
|
|
20
|
+
'.git', '.venv', 'venv', 'env', '.env', 'node_modules', '__pycache__', 'site-packages',
|
|
21
|
+
'.tox', '.mypy_cache', '.pytest_cache', 'dist', 'build', 'migrations',
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* Analyze a Python web app's routes
|
|
25
|
+
*/
|
|
26
|
+
export async function analyzePython(options) {
|
|
27
|
+
let appDir = options.appDir || process.cwd();
|
|
28
|
+
let { app, factory, djangoSettings } = options;
|
|
29
|
+
// Fill in whatever the caller left out from the project layout.
|
|
30
|
+
if (options.framework === 'django' ? !app && !djangoSettings && !process.env.DJANGO_SETTINGS_MODULE : !app) {
|
|
31
|
+
const [candidate] = findPythonApps(options.framework, appDir);
|
|
32
|
+
if (!candidate) {
|
|
33
|
+
throw new Error(options.framework === 'django'
|
|
34
|
+
? 'Django settings not found — pass djangoSettings (e.g. "mysite.settings")'
|
|
35
|
+
: `${options.framework} app not found — pass pythonApp (e.g. "main:app")`);
|
|
36
|
+
}
|
|
37
|
+
;
|
|
38
|
+
({ app, factory, djangoSettings, appDir } = candidate);
|
|
39
|
+
}
|
|
40
|
+
const pythonPath = options.pythonPath || findPythonInterpreter(appDir);
|
|
41
|
+
const stdout = await runPython(pythonPath, appDir, {
|
|
42
|
+
framework: options.framework, app, factory, djangoSettings, appDir,
|
|
43
|
+
}, options.timeoutMs ?? 60000);
|
|
44
|
+
const line = stdout.split(/\r?\n/).find(l => l.startsWith(ROUTES_MARKER));
|
|
45
|
+
if (!line)
|
|
46
|
+
throw new Error('Python analyzer produced no output');
|
|
47
|
+
return JSON.parse(line.slice(ROUTES_MARKER.length));
|
|
48
|
+
}
|
|
49
|
+
function runPython(pythonPath, cwd, opts, timeoutMs) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const child = spawn(pythonPath, ['-'], {
|
|
52
|
+
cwd,
|
|
53
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
54
|
+
env: {
|
|
55
|
+
...process.env,
|
|
56
|
+
APIFORGE_PY_OPTIONS: JSON.stringify(opts),
|
|
57
|
+
PYTHONIOENCODING: 'utf-8',
|
|
58
|
+
// Don't leave __pycache__ folders behind in the user's project.
|
|
59
|
+
PYTHONDONTWRITEBYTECODE: '1',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
let stdout = '';
|
|
63
|
+
let stderr = '';
|
|
64
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString('utf-8'); });
|
|
65
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf-8'); });
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
child.kill();
|
|
68
|
+
reject(new Error(`Python analyzer timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
69
|
+
}, timeoutMs);
|
|
70
|
+
child.on('error', (err) => {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
reject(new Error(err.code === 'ENOENT'
|
|
73
|
+
? `Python interpreter not found: "${pythonPath}" — activate your virtualenv or set APIFORGE_PYTHON`
|
|
74
|
+
: err.message));
|
|
75
|
+
});
|
|
76
|
+
child.on('close', code => {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
if (code === 0)
|
|
79
|
+
return resolve(stdout);
|
|
80
|
+
// The last traceback line is the actual error (ModuleNotFoundError: ..., etc.)
|
|
81
|
+
const lastLine = stderr.trim().split(/\r?\n/).pop() || `exited with code ${code}`;
|
|
82
|
+
reject(new Error(`Python analyzer failed: ${lastLine}`));
|
|
83
|
+
});
|
|
84
|
+
child.stdin.end(PYTHON_INTROSPECT_SCRIPT);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The interpreter that has the project's dependencies installed:
|
|
89
|
+
* APIFORGE_PYTHON → active virtualenv → a venv inside the project → system python.
|
|
90
|
+
*/
|
|
91
|
+
export function findPythonInterpreter(projectDir = process.cwd()) {
|
|
92
|
+
if (process.env.APIFORGE_PYTHON)
|
|
93
|
+
return process.env.APIFORGE_PYTHON;
|
|
94
|
+
const inVenv = (venvDir) => process.platform === 'win32'
|
|
95
|
+
? path.join(venvDir, 'Scripts', 'python.exe')
|
|
96
|
+
: path.join(venvDir, 'bin', 'python');
|
|
97
|
+
const candidates = [
|
|
98
|
+
process.env.VIRTUAL_ENV,
|
|
99
|
+
...['.venv', 'venv', 'env'].map(dir => path.join(projectDir, dir)),
|
|
100
|
+
].filter(Boolean).map(dir => inVenv(dir));
|
|
101
|
+
return candidates.find(p => fs.existsSync(p)) || (process.platform === 'win32' ? 'python' : 'python3');
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Find where the app object (FastAPI/Flask) or settings module (Django) lives.
|
|
105
|
+
*/
|
|
106
|
+
export function findPythonApps(framework, projectDir = process.cwd()) {
|
|
107
|
+
const files = listPythonFiles(projectDir, 3);
|
|
108
|
+
if (framework === 'django') {
|
|
109
|
+
const candidates = [];
|
|
110
|
+
for (const file of files.filter(f => path.basename(f) === 'manage.py')) {
|
|
111
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
112
|
+
const match = content.match(/DJANGO_SETTINGS_MODULE['"]\s*,\s*['"]([\w.]+)['"]/);
|
|
113
|
+
if (match) {
|
|
114
|
+
const appDir = path.dirname(file);
|
|
115
|
+
// The CLI injects into manage.py itself: every runserver goes through it, whereas
|
|
116
|
+
// wsgi.py is only loaded when settings.WSGI_APPLICATION points at it.
|
|
117
|
+
candidates.push({ label: `${match[1]} (${path.relative(projectDir, file) || 'manage.py'})`, djangoSettings: match[1], appDir, file });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return candidates;
|
|
121
|
+
}
|
|
122
|
+
const ctor = framework === 'fastapi' ? 'FastAPI' : 'Flask';
|
|
123
|
+
const instanceRe = new RegExp(`^(\\w+)\\s*(?::\\s*\\w+\\s*)?=\\s*(?:\\w+\\.)?${ctor}\\(`, 'gm');
|
|
124
|
+
const candidates = [];
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
127
|
+
if (!content.includes(ctor))
|
|
128
|
+
continue;
|
|
129
|
+
// src/ layout: import from inside src/ so the app's own absolute imports resolve
|
|
130
|
+
const rel = path.relative(projectDir, file).split(path.sep);
|
|
131
|
+
const appDir = rel[0] === 'src' ? path.join(projectDir, 'src') : projectDir;
|
|
132
|
+
if (rel[0] === 'src')
|
|
133
|
+
rel.shift();
|
|
134
|
+
const module = rel.join('.').replace(/\.py$/, '').replace(/\.__init__$/, '');
|
|
135
|
+
for (const match of content.matchAll(instanceRe)) {
|
|
136
|
+
candidates.push({ label: `${module}:${match[1]}`, app: `${module}:${match[1]}`, appDir, file });
|
|
137
|
+
}
|
|
138
|
+
if (framework === 'flask' && !content.match(instanceRe)) {
|
|
139
|
+
const factory = content.match(/^def\s+(create_app|make_app)\s*\(/m);
|
|
140
|
+
if (factory)
|
|
141
|
+
candidates.push({ label: `${module}:${factory[1]}()`, app: `${module}:${factory[1]}`, factory: true, appDir, file });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Top-level entry files first: main.py / app.py beat a FastAPI() in some sub-module
|
|
145
|
+
return candidates.sort((a, b) => depth(a.app) - depth(b.app));
|
|
146
|
+
}
|
|
147
|
+
function depth(app) {
|
|
148
|
+
return (app || '').split(':')[0].split('.').length;
|
|
149
|
+
}
|
|
150
|
+
function listPythonFiles(dir, maxDepth, level = 0) {
|
|
151
|
+
let entries;
|
|
152
|
+
try {
|
|
153
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
const files = [];
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
const full = path.join(dir, entry.name);
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
if (level < maxDepth && !SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
163
|
+
files.push(...listPythonFiles(full, maxDepth, level + 1));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (entry.isFile() && entry.name.endsWith('.py')) {
|
|
167
|
+
files.push(full);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return files;
|
|
171
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { analyzeVite } from './frameworks/vite.js';
|
|
|
10
10
|
import { analyzeReactRouter } from './frameworks/react-router.js';
|
|
11
11
|
import { analyzeNestJS } from './frameworks/nestjs.js';
|
|
12
12
|
import { analyzeFastify } from './frameworks/fastify.js';
|
|
13
|
+
import { analyzePython } from './frameworks/python.js';
|
|
13
14
|
import { detectFramework, SUPPORTED_FRAMEWORKS } from './detect.js';
|
|
14
15
|
import { exportToJSON } from './export.js';
|
|
15
16
|
import { uploadRoutes, writeStatusFile } from './uploader.js';
|
|
@@ -167,6 +168,18 @@ export async function analyze(app, options) {
|
|
|
167
168
|
case 'react-router':
|
|
168
169
|
routes = await analyzeReactRouter(options.nextDir || 'src/routes.tsx');
|
|
169
170
|
break;
|
|
171
|
+
case 'fastapi':
|
|
172
|
+
case 'flask':
|
|
173
|
+
case 'django':
|
|
174
|
+
routes = await analyzePython({
|
|
175
|
+
framework: framework,
|
|
176
|
+
app: options.pythonApp,
|
|
177
|
+
factory: options.pythonFactory,
|
|
178
|
+
pythonPath: options.pythonPath,
|
|
179
|
+
djangoSettings: options.djangoSettings,
|
|
180
|
+
appDir: options.nextDir,
|
|
181
|
+
});
|
|
182
|
+
break;
|
|
170
183
|
// No default: unsupported frameworks are rejected above, before this
|
|
171
184
|
// try block, so a thrown error here isn't silently swallowed into `[]`.
|
|
172
185
|
}
|
|
@@ -212,7 +225,7 @@ export async function analyze(app, options) {
|
|
|
212
225
|
return routes;
|
|
213
226
|
}
|
|
214
227
|
// Re-exports for library consumers
|
|
215
|
-
export { analyzeExpress, analyzeNextPages, analyzeNextApp, analyzeVite, analyzeReactRouter, analyzeNestJS, analyzeFastify };
|
|
228
|
+
export { analyzeExpress, analyzeNextPages, analyzeNextApp, analyzeVite, analyzeReactRouter, analyzeNestJS, analyzeFastify, analyzePython };
|
|
216
229
|
export { exportToJSON } from './export.js';
|
|
217
230
|
export { uploadRoutes } from './uploader.js';
|
|
218
231
|
export { detectFramework } from './detect.js';
|
|
@@ -226,6 +239,7 @@ export default {
|
|
|
226
239
|
analyzeVite,
|
|
227
240
|
analyzeReactRouter,
|
|
228
241
|
analyzeNestJS,
|
|
242
|
+
analyzePython,
|
|
229
243
|
exportToJSON,
|
|
230
244
|
uploadRoutes,
|
|
231
245
|
detectFramework,
|
package/dist/types/detect.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type Framework = 'express' | 'fastify' | 'koa' | 'hapi' | 'nestjs' | 'nextjs-pages' | 'nextjs-app' | 'vite' | 'react-router' | 'unknown';
|
|
1
|
+
export type Framework = 'express' | 'fastify' | 'koa' | 'hapi' | 'nestjs' | 'nextjs-pages' | 'nextjs-app' | 'vite' | 'react-router' | 'fastapi' | 'flask' | 'django' | 'unknown';
|
|
2
2
|
export interface FrameworkInfo {
|
|
3
3
|
name: Framework;
|
|
4
4
|
version?: string;
|
|
@@ -13,3 +13,14 @@ export interface FrameworkInfo {
|
|
|
13
13
|
*/
|
|
14
14
|
export declare const SUPPORTED_FRAMEWORKS: ReadonlySet<Framework>;
|
|
15
15
|
export declare function detectFramework(app?: any): FrameworkInfo;
|
|
16
|
+
/**
|
|
17
|
+
* Detect a Python web framework from manage.py / dependency files.
|
|
18
|
+
* Python apps are never passed to analyze() as an object, so this is file-based only.
|
|
19
|
+
*/
|
|
20
|
+
export declare function detectPythonFramework(dir?: string): 'fastapi' | 'flask' | 'django' | null;
|
|
21
|
+
/**
|
|
22
|
+
* Read a text file honouring a UTF-16 BOM. Windows PowerShell 5.1's
|
|
23
|
+
* `pip freeze > requirements.txt` writes UTF-16 LE, which read as UTF-8
|
|
24
|
+
* interleaves NUL bytes and hides every package name.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readTextFile(path: string): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python side of the FastAPI / Flask / Django analyzers (see python.ts).
|
|
3
|
+
*
|
|
4
|
+
* Reads the framework's live route table — the Python equivalent of walking
|
|
5
|
+
* app._router.stack in express.ts. Runs in one of two modes:
|
|
6
|
+
* - analyze(): piped to the user's interpreter on stdin; imports the app and
|
|
7
|
+
* prints raw Route objects as one JSON line prefixed with ROUTES_MARKER.
|
|
8
|
+
* - CLI: exec'd by the snippet the CLI injects into the user's entry file
|
|
9
|
+
* (APIFORGE_APP set), inside their own running server; writes the routes
|
|
10
|
+
* to APIFORGE_STATUS_FILE, like the Node snippet's collection mode.
|
|
11
|
+
* Enrichment and upload stay in TS either way.
|
|
12
|
+
*
|
|
13
|
+
* Kept as an embedded string (not a .py file) so the existing tsc-only build
|
|
14
|
+
* ships it without any copy step. String.raw keeps regex backslashes intact —
|
|
15
|
+
* the Python below must not contain backticks or a dollar-brace sequence.
|
|
16
|
+
*/
|
|
17
|
+
export declare const ROUTES_MARKER = "__APIFORGE_ROUTES__";
|
|
18
|
+
export declare const PYTHON_INTROSPECT_SCRIPT: string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FastAPI / Flask / Django (+ DRF) analyzer
|
|
3
|
+
*
|
|
4
|
+
* Python apps can't be handed to analyze() as a JS object, so this runs the
|
|
5
|
+
* user's own interpreter (their venv, so their dependencies resolve), imports
|
|
6
|
+
* the app, and reads the framework's live route table — see
|
|
7
|
+
* python-introspect.ts. Nothing is injected into the user's code and no server
|
|
8
|
+
* is started. Returns the same Route[] as every other analyzer; enrichment and
|
|
9
|
+
* upload happen in index.ts / uploader.ts exactly as for Express or Fastify.
|
|
10
|
+
*
|
|
11
|
+
* The `npx apiforge` CLI instead works like its Node flow: it injects a snippet
|
|
12
|
+
* into the file returned in PythonAppCandidate.file and runs the user's own
|
|
13
|
+
* start command (see pythonFlow in cli.ts).
|
|
14
|
+
*/
|
|
15
|
+
import { Route } from '../export.js';
|
|
16
|
+
export type PythonFramework = 'fastapi' | 'flask' | 'django';
|
|
17
|
+
export interface PythonAnalyzerOptions {
|
|
18
|
+
framework: PythonFramework;
|
|
19
|
+
/** App import path, e.g. 'main:app', 'app:create_app' (with factory), 'mysite.wsgi:application'. */
|
|
20
|
+
app?: string;
|
|
21
|
+
/** Call `app` as a factory (Flask's create_app pattern). */
|
|
22
|
+
factory?: boolean;
|
|
23
|
+
/** Django settings module, e.g. 'mysite.settings'. Defaults to DJANGO_SETTINGS_MODULE / manage.py. */
|
|
24
|
+
djangoSettings?: string;
|
|
25
|
+
/** Interpreter to run. Defaults to findPythonInterpreter(). */
|
|
26
|
+
pythonPath?: string;
|
|
27
|
+
/** Directory the app is imported from. Defaults to process.cwd(). */
|
|
28
|
+
appDir?: string;
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface PythonAppCandidate {
|
|
32
|
+
label: string;
|
|
33
|
+
app?: string;
|
|
34
|
+
factory?: boolean;
|
|
35
|
+
djangoSettings?: string;
|
|
36
|
+
appDir: string;
|
|
37
|
+
/** File the CLI injects its snippet into: the module defining the app, or Django's manage.py. */
|
|
38
|
+
file?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Analyze a Python web app's routes
|
|
42
|
+
*/
|
|
43
|
+
export declare function analyzePython(options: PythonAnalyzerOptions): Promise<Route[]>;
|
|
44
|
+
/**
|
|
45
|
+
* The interpreter that has the project's dependencies installed:
|
|
46
|
+
* APIFORGE_PYTHON → active virtualenv → a venv inside the project → system python.
|
|
47
|
+
*/
|
|
48
|
+
export declare function findPythonInterpreter(projectDir?: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Find where the app object (FastAPI/Flask) or settings module (Django) lives.
|
|
51
|
+
*/
|
|
52
|
+
export declare function findPythonApps(framework: PythonFramework, projectDir?: string): PythonAppCandidate[];
|
package/dist/types/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { analyzeVite } from './frameworks/vite.js';
|
|
|
8
8
|
import { analyzeReactRouter } from './frameworks/react-router.js';
|
|
9
9
|
import { analyzeNestJS } from './frameworks/nestjs.js';
|
|
10
10
|
import { analyzeFastify } from './frameworks/fastify.js';
|
|
11
|
+
import { analyzePython } from './frameworks/python.js';
|
|
11
12
|
import { detectFramework } from './detect.js';
|
|
12
13
|
import { exportToJSON } from './export.js';
|
|
13
14
|
import { uploadRoutes } from './uploader.js';
|
|
@@ -41,9 +42,17 @@ export interface AnalyzerOptions {
|
|
|
41
42
|
}>;
|
|
42
43
|
/** JSON file keyed by "METHOD /path" carrying summary/description/tags/deprecated/errors overrides for what can't be inferred. */
|
|
43
44
|
annotationsPath?: string;
|
|
45
|
+
/** App import path, e.g. 'main:app' or 'app:create_app'. Auto-detected when omitted. */
|
|
46
|
+
pythonApp?: string;
|
|
47
|
+
/** Treat pythonApp as an app factory and call it (Flask create_app pattern). */
|
|
48
|
+
pythonFactory?: boolean;
|
|
49
|
+
/** Python interpreter with the app's dependencies. Defaults to APIFORGE_PYTHON / the project's venv. */
|
|
50
|
+
pythonPath?: string;
|
|
51
|
+
/** Django settings module, e.g. 'mysite.settings'. Defaults to the one manage.py sets. */
|
|
52
|
+
djangoSettings?: string;
|
|
44
53
|
}
|
|
45
54
|
export declare function analyze(app: any, options: AnalyzerOptions): Promise<any[]>;
|
|
46
|
-
export { analyzeExpress, analyzeNextPages, analyzeNextApp, analyzeVite, analyzeReactRouter, analyzeNestJS, analyzeFastify };
|
|
55
|
+
export { analyzeExpress, analyzeNextPages, analyzeNextApp, analyzeVite, analyzeReactRouter, analyzeNestJS, analyzeFastify, analyzePython };
|
|
47
56
|
export { exportToJSON } from './export.js';
|
|
48
57
|
export { uploadRoutes } from './uploader.js';
|
|
49
58
|
export type { Route } from './export.js';
|
|
@@ -58,6 +67,7 @@ declare const _default: {
|
|
|
58
67
|
analyzeVite: typeof analyzeVite;
|
|
59
68
|
analyzeReactRouter: typeof analyzeReactRouter;
|
|
60
69
|
analyzeNestJS: typeof analyzeNestJS;
|
|
70
|
+
analyzePython: typeof analyzePython;
|
|
61
71
|
exportToJSON: typeof exportToJSON;
|
|
62
72
|
uploadRoutes: typeof uploadRoutes;
|
|
63
73
|
detectFramework: typeof detectFramework;
|
package/package.json
CHANGED