@shrkcrft/framework-scanners 0.1.0-alpha.10
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/dist/extractor-api/extractor-registry.d.ts +19 -0
- package/dist/extractor-api/extractor-registry.d.ts.map +1 -0
- package/dist/extractor-api/extractor-registry.js +36 -0
- package/dist/extractor-api/framework-extractor.d.ts +48 -0
- package/dist/extractor-api/framework-extractor.d.ts.map +1 -0
- package/dist/extractor-api/framework-extractor.js +1 -0
- package/dist/extractors/angular-extractor.d.ts +18 -0
- package/dist/extractors/angular-extractor.d.ts.map +1 -0
- package/dist/extractors/angular-extractor.js +175 -0
- package/dist/extractors/astro-extractor.d.ts +15 -0
- package/dist/extractors/astro-extractor.d.ts.map +1 -0
- package/dist/extractors/astro-extractor.js +128 -0
- package/dist/extractors/django-extractor.d.ts +24 -0
- package/dist/extractors/django-extractor.d.ts.map +1 -0
- package/dist/extractors/django-extractor.js +124 -0
- package/dist/extractors/express-extractor.d.ts +18 -0
- package/dist/extractors/express-extractor.d.ts.map +1 -0
- package/dist/extractors/express-extractor.js +193 -0
- package/dist/extractors/fastapi-extractor.d.ts +19 -0
- package/dist/extractors/fastapi-extractor.d.ts.map +1 -0
- package/dist/extractors/fastapi-extractor.js +135 -0
- package/dist/extractors/fastify-extractor.d.ts +13 -0
- package/dist/extractors/fastify-extractor.d.ts.map +1 -0
- package/dist/extractors/fastify-extractor.js +166 -0
- package/dist/extractors/flask-extractor.d.ts +16 -0
- package/dist/extractors/flask-extractor.d.ts.map +1 -0
- package/dist/extractors/flask-extractor.js +142 -0
- package/dist/extractors/flutter-extractor.d.ts +26 -0
- package/dist/extractors/flutter-extractor.d.ts.map +1 -0
- package/dist/extractors/flutter-extractor.js +137 -0
- package/dist/extractors/graphql-extractor.d.ts +27 -0
- package/dist/extractors/graphql-extractor.d.ts.map +1 -0
- package/dist/extractors/graphql-extractor.js +141 -0
- package/dist/extractors/laravel-extractor.d.ts +22 -0
- package/dist/extractors/laravel-extractor.d.ts.map +1 -0
- package/dist/extractors/laravel-extractor.js +208 -0
- package/dist/extractors/nestjs-extractor.d.ts +18 -0
- package/dist/extractors/nestjs-extractor.d.ts.map +1 -0
- package/dist/extractors/nestjs-extractor.js +222 -0
- package/dist/extractors/nextjs-extractor.d.ts +19 -0
- package/dist/extractors/nextjs-extractor.d.ts.map +1 -0
- package/dist/extractors/nextjs-extractor.js +175 -0
- package/dist/extractors/phoenix-extractor.d.ts +28 -0
- package/dist/extractors/phoenix-extractor.d.ts.map +1 -0
- package/dist/extractors/phoenix-extractor.js +212 -0
- package/dist/extractors/rails-extractor.d.ts +25 -0
- package/dist/extractors/rails-extractor.d.ts.map +1 -0
- package/dist/extractors/rails-extractor.js +180 -0
- package/dist/extractors/react-extractor.d.ts +19 -0
- package/dist/extractors/react-extractor.d.ts.map +1 -0
- package/dist/extractors/react-extractor.js +209 -0
- package/dist/extractors/solid-extractor.d.ts +19 -0
- package/dist/extractors/solid-extractor.d.ts.map +1 -0
- package/dist/extractors/solid-extractor.js +164 -0
- package/dist/extractors/spring-extractor.d.ts +27 -0
- package/dist/extractors/spring-extractor.d.ts.map +1 -0
- package/dist/extractors/spring-extractor.js +279 -0
- package/dist/extractors/svelte-extractor.d.ts +17 -0
- package/dist/extractors/svelte-extractor.d.ts.map +1 -0
- package/dist/extractors/svelte-extractor.js +104 -0
- package/dist/extractors/vue-extractor.d.ts +18 -0
- package/dist/extractors/vue-extractor.d.ts.map +1 -0
- package/dist/extractors/vue-extractor.js +125 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/query/framework-query-api.d.ts +39 -0
- package/dist/query/framework-query-api.d.ts.map +1 -0
- package/dist/query/framework-query-api.js +99 -0
- package/dist/runner/load-pack-extractors.d.ts +36 -0
- package/dist/runner/load-pack-extractors.d.ts.map +1 -0
- package/dist/runner/load-pack-extractors.js +87 -0
- package/dist/runner/run-extractors.d.ts +29 -0
- package/dist/runner/run-extractors.d.ts.map +1 -0
- package/dist/runner/run-extractors.js +144 -0
- package/dist/schema/framework-schema.d.ts +36 -0
- package/dist/schema/framework-schema.d.ts.map +1 -0
- package/dist/schema/framework-schema.js +1 -0
- package/dist/store/framework-store.d.ts +17 -0
- package/dist/store/framework-store.d.ts.map +1 -0
- package/dist/store/framework-store.js +138 -0
- package/package.json +55 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
3
|
+
import * as ts from 'typescript';
|
|
4
|
+
import { EdgeKind, NodeKind } from '@shrkcrft/graph';
|
|
5
|
+
export const EXPRESS_EXTRACTOR_SOURCE = 'express-extractor@v1';
|
|
6
|
+
const HTTP_METHOD_NAMES = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'all']);
|
|
7
|
+
// CommonJS-style detection uses a split string for the `require(` token
|
|
8
|
+
// so the import-hygiene scanner doesn't flag the needle itself as a
|
|
9
|
+
// lazy `require('node:*')`.
|
|
10
|
+
const REQ = 'requ' + 'ire';
|
|
11
|
+
const FAST_FILTER_NEEDLES = [
|
|
12
|
+
"from 'express'",
|
|
13
|
+
'from "express"',
|
|
14
|
+
`${REQ}('express')`,
|
|
15
|
+
`${REQ}("express")`,
|
|
16
|
+
'express.Router(',
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Express extractor.
|
|
20
|
+
*
|
|
21
|
+
* Express has no decorators, so detection is signature-based:
|
|
22
|
+
* - A variable initialized with a call to `express()` or
|
|
23
|
+
* `express.Router()` (or to `Router(...)` when `Router` was
|
|
24
|
+
* imported from `'express'`) becomes a **router** entity.
|
|
25
|
+
* - Subsequent `<router>.get(path, …)` etc. calls become **route**
|
|
26
|
+
* entities with method + path data.
|
|
27
|
+
*
|
|
28
|
+
* Middleware chains (extra handlers in the call args) are recorded on
|
|
29
|
+
* the route entity's `data.middlewareCount`. Detailed middleware-node
|
|
30
|
+
* extraction is out of scope for the MVP.
|
|
31
|
+
*/
|
|
32
|
+
export const expressExtractor = {
|
|
33
|
+
framework: 'express',
|
|
34
|
+
label: 'Express',
|
|
35
|
+
fileMatches({ path, content }) {
|
|
36
|
+
if (!/\.(?:t|j)sx?$/.test(path))
|
|
37
|
+
return false;
|
|
38
|
+
for (const needle of FAST_FILTER_NEEDLES) {
|
|
39
|
+
if (content.includes(needle))
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
},
|
|
44
|
+
extract(input) {
|
|
45
|
+
const nodes = [];
|
|
46
|
+
const edges = [];
|
|
47
|
+
const sf = parse(input);
|
|
48
|
+
if (!sf)
|
|
49
|
+
return { nodes, edges };
|
|
50
|
+
// 1. Find router bindings: variable names initialized with
|
|
51
|
+
// `express()`, `express.Router()`, or `Router()` from express.
|
|
52
|
+
const routerNames = new Set();
|
|
53
|
+
const routerNodeByName = new Map();
|
|
54
|
+
const namedRouterImports = collectNamedRouterImports(sf);
|
|
55
|
+
const collectRouters = (node) => {
|
|
56
|
+
if (ts.isVariableStatement(node)) {
|
|
57
|
+
for (const decl of node.declarationList.declarations) {
|
|
58
|
+
if (!ts.isIdentifier(decl.name))
|
|
59
|
+
continue;
|
|
60
|
+
const init = decl.initializer;
|
|
61
|
+
if (!init)
|
|
62
|
+
continue;
|
|
63
|
+
if (isRouterFactory(init, namedRouterImports)) {
|
|
64
|
+
const name = decl.name.text;
|
|
65
|
+
routerNames.add(name);
|
|
66
|
+
const entity = makeRouterEntity(input, name);
|
|
67
|
+
routerNodeByName.set(name, entity);
|
|
68
|
+
nodes.push(entity);
|
|
69
|
+
edges.push(edge(input.fileNodeId, entity.id, EdgeKind.FrameworkDeclares, { subtype: 'router' }));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
ts.forEachChild(node, collectRouters);
|
|
74
|
+
};
|
|
75
|
+
ts.forEachChild(sf, collectRouters);
|
|
76
|
+
// 2. Walk every CallExpression matching <name>.<method>(<path>, ...).
|
|
77
|
+
const collectRoutes = (node) => {
|
|
78
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
79
|
+
const objExpr = node.expression.expression;
|
|
80
|
+
const method = node.expression.name.text;
|
|
81
|
+
if (HTTP_METHOD_NAMES.has(method) && ts.isIdentifier(objExpr) && routerNames.has(objExpr.text)) {
|
|
82
|
+
const arg0 = node.arguments[0];
|
|
83
|
+
if (arg0 && ts.isStringLiteral(arg0)) {
|
|
84
|
+
const path = arg0.text;
|
|
85
|
+
const router = routerNodeByName.get(objExpr.text);
|
|
86
|
+
const route = makeRouteEntity(input, objExpr.text, method.toUpperCase(), path, node.arguments.length - 1);
|
|
87
|
+
nodes.push(route);
|
|
88
|
+
edges.push(edge(router.id, route.id, EdgeKind.HandlesRoute, {
|
|
89
|
+
method: method.toUpperCase(),
|
|
90
|
+
path,
|
|
91
|
+
}));
|
|
92
|
+
edges.push(edge(input.fileNodeId, route.id, EdgeKind.FrameworkDeclares, { subtype: 'route' }));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
ts.forEachChild(node, collectRoutes);
|
|
97
|
+
};
|
|
98
|
+
ts.forEachChild(sf, collectRoutes);
|
|
99
|
+
return { nodes, edges };
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
function collectNamedRouterImports(sf) {
|
|
103
|
+
// Local names bound to `Router` imported from `'express'`.
|
|
104
|
+
const out = new Set();
|
|
105
|
+
for (const stmt of sf.statements) {
|
|
106
|
+
if (!ts.isImportDeclaration(stmt))
|
|
107
|
+
continue;
|
|
108
|
+
if (!ts.isStringLiteral(stmt.moduleSpecifier) || stmt.moduleSpecifier.text !== 'express')
|
|
109
|
+
continue;
|
|
110
|
+
const clause = stmt.importClause;
|
|
111
|
+
if (!clause)
|
|
112
|
+
continue;
|
|
113
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
114
|
+
for (const elem of clause.namedBindings.elements) {
|
|
115
|
+
const propertyName = elem.propertyName ? elem.propertyName.text : elem.name.text;
|
|
116
|
+
if (propertyName === 'Router')
|
|
117
|
+
out.add(elem.name.text);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
function isRouterFactory(node, namedRouterImports) {
|
|
124
|
+
if (!ts.isCallExpression(node))
|
|
125
|
+
return false;
|
|
126
|
+
const callee = node.expression;
|
|
127
|
+
if (ts.isIdentifier(callee)) {
|
|
128
|
+
if (callee.text === 'express')
|
|
129
|
+
return true;
|
|
130
|
+
if (namedRouterImports.has(callee.text))
|
|
131
|
+
return true;
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
135
|
+
// express.Router()
|
|
136
|
+
if (ts.isIdentifier(callee.expression) &&
|
|
137
|
+
callee.expression.text === 'express' &&
|
|
138
|
+
callee.name.text === 'Router') {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
function makeRouterEntity(input, name) {
|
|
145
|
+
return {
|
|
146
|
+
id: `framework:express:router:${input.filePath}#${name}`,
|
|
147
|
+
kind: NodeKind.FrameworkEntity,
|
|
148
|
+
label: name,
|
|
149
|
+
path: input.filePath,
|
|
150
|
+
tags: ['express', 'router'],
|
|
151
|
+
data: { framework: 'express', subtype: 'router', name },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function makeRouteEntity(input, routerName, method, path, middlewareCount) {
|
|
155
|
+
return {
|
|
156
|
+
id: `framework:express:route:${input.filePath}#${routerName}#${method}:${path}`,
|
|
157
|
+
kind: NodeKind.FrameworkEntity,
|
|
158
|
+
label: `${method} ${path}`,
|
|
159
|
+
path: input.filePath,
|
|
160
|
+
tags: ['express', 'route'],
|
|
161
|
+
data: {
|
|
162
|
+
framework: 'express',
|
|
163
|
+
subtype: 'route',
|
|
164
|
+
method,
|
|
165
|
+
path,
|
|
166
|
+
router: routerName,
|
|
167
|
+
middlewareCount,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function edge(from, to, kind, data) {
|
|
172
|
+
return {
|
|
173
|
+
id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
|
|
174
|
+
from,
|
|
175
|
+
to,
|
|
176
|
+
kind,
|
|
177
|
+
source: EXPRESS_EXTRACTOR_SOURCE,
|
|
178
|
+
...(data ? { data } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function parse(input) {
|
|
182
|
+
const ext = nodePath.extname(input.filePath).toLowerCase();
|
|
183
|
+
const kind = ext === '.tsx' ? ts.ScriptKind.TSX
|
|
184
|
+
: ext === '.jsx' ? ts.ScriptKind.JSX
|
|
185
|
+
: ext === '.js' || ext === '.mjs' || ext === '.cjs' ? ts.ScriptKind.JS
|
|
186
|
+
: ts.ScriptKind.TS;
|
|
187
|
+
try {
|
|
188
|
+
return ts.createSourceFile(input.filePath, input.content, ts.ScriptTarget.Latest, true, kind);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IFrameworkExtractor } from '../extractor-api/framework-extractor.js';
|
|
2
|
+
export declare const FASTAPI_EXTRACTOR_SOURCE = "fastapi-extractor@v1";
|
|
3
|
+
/**
|
|
4
|
+
* FastAPI extractor.
|
|
5
|
+
*
|
|
6
|
+
* Regex-only (Python is not parsed via the TS AST). Detection:
|
|
7
|
+
* - `<name> = FastAPI(...)` and `<name> = APIRouter(...)` → app /
|
|
8
|
+
* router entity.
|
|
9
|
+
* - `@<name>.<method>('<path>', ...)` → route entity, with the
|
|
10
|
+
* decorated function captured on the next non-blank, non-decorator
|
|
11
|
+
* line as the handler name.
|
|
12
|
+
*
|
|
13
|
+
* Out of scope (Python-only follow-ups):
|
|
14
|
+
* - Path-operation function parameters (Pydantic body / query / path).
|
|
15
|
+
* - Dependency injection (Depends()).
|
|
16
|
+
* - Nested router includes (include_router).
|
|
17
|
+
*/
|
|
18
|
+
export declare const fastapiExtractor: IFrameworkExtractor;
|
|
19
|
+
//# sourceMappingURL=fastapi-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fastapi-extractor.d.ts","sourceRoot":"","sources":["../../src/extractors/fastapi-extractor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,yCAAyC,CAAC;AAEjD,eAAO,MAAM,wBAAwB,yBAAyB,CAAC;AAW/D;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,EAAE,mBAmE9B,CAAC"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { EdgeKind, NodeKind } from '@shrkcrft/graph';
|
|
3
|
+
export const FASTAPI_EXTRACTOR_SOURCE = 'fastapi-extractor@v1';
|
|
4
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'];
|
|
5
|
+
const FAST_FILTER_NEEDLES = [
|
|
6
|
+
'from fastapi',
|
|
7
|
+
'fastapi import',
|
|
8
|
+
'FastAPI(',
|
|
9
|
+
'APIRouter(',
|
|
10
|
+
];
|
|
11
|
+
/**
|
|
12
|
+
* FastAPI extractor.
|
|
13
|
+
*
|
|
14
|
+
* Regex-only (Python is not parsed via the TS AST). Detection:
|
|
15
|
+
* - `<name> = FastAPI(...)` and `<name> = APIRouter(...)` → app /
|
|
16
|
+
* router entity.
|
|
17
|
+
* - `@<name>.<method>('<path>', ...)` → route entity, with the
|
|
18
|
+
* decorated function captured on the next non-blank, non-decorator
|
|
19
|
+
* line as the handler name.
|
|
20
|
+
*
|
|
21
|
+
* Out of scope (Python-only follow-ups):
|
|
22
|
+
* - Path-operation function parameters (Pydantic body / query / path).
|
|
23
|
+
* - Dependency injection (Depends()).
|
|
24
|
+
* - Nested router includes (include_router).
|
|
25
|
+
*/
|
|
26
|
+
export const fastapiExtractor = {
|
|
27
|
+
framework: 'fastapi',
|
|
28
|
+
label: 'FastAPI',
|
|
29
|
+
fileMatches({ path, content }) {
|
|
30
|
+
if (!path.endsWith('.py'))
|
|
31
|
+
return false;
|
|
32
|
+
for (const needle of FAST_FILTER_NEEDLES) {
|
|
33
|
+
if (content.includes(needle))
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
},
|
|
38
|
+
extract(input) {
|
|
39
|
+
const nodes = [];
|
|
40
|
+
const edges = [];
|
|
41
|
+
const appNames = new Map();
|
|
42
|
+
const lines = input.content.split('\n');
|
|
43
|
+
// First pass: find `<name> = FastAPI(` / `APIRouter(`.
|
|
44
|
+
const appRe = /^([A-Za-z_]\w*)\s*=\s*(FastAPI|APIRouter)\s*\(/;
|
|
45
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
46
|
+
const m = appRe.exec(lines[i]);
|
|
47
|
+
if (!m)
|
|
48
|
+
continue;
|
|
49
|
+
const name = m[1];
|
|
50
|
+
const kind = m[2] === 'FastAPI' ? 'app' : 'router';
|
|
51
|
+
const node = makeAppEntity(input, name, kind);
|
|
52
|
+
appNames.set(name, node);
|
|
53
|
+
nodes.push(node);
|
|
54
|
+
edges.push(edge(input.fileNodeId, node.id, EdgeKind.FrameworkDeclares, { subtype: kind }));
|
|
55
|
+
}
|
|
56
|
+
if (appNames.size === 0)
|
|
57
|
+
return { nodes, edges };
|
|
58
|
+
// Second pass: decorators on the line preceding a `def` / `async def`.
|
|
59
|
+
const decoratorRe = /^@([A-Za-z_]\w*)\.([a-z]+)\s*\(\s*['"]([^'"]+)['"]/;
|
|
60
|
+
const defRe = /^(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/;
|
|
61
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
62
|
+
const dm = decoratorRe.exec(lines[i]);
|
|
63
|
+
if (!dm)
|
|
64
|
+
continue;
|
|
65
|
+
const appName = dm[1];
|
|
66
|
+
const method = dm[2];
|
|
67
|
+
const path = dm[3];
|
|
68
|
+
if (!appNames.has(appName))
|
|
69
|
+
continue;
|
|
70
|
+
if (!HTTP_METHODS.includes(method))
|
|
71
|
+
continue;
|
|
72
|
+
// Find the handler def — scan forward through additional decorators.
|
|
73
|
+
let handler = 'handler';
|
|
74
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
75
|
+
const line = lines[j];
|
|
76
|
+
const trimmed = line.trimStart();
|
|
77
|
+
if (trimmed.length === 0)
|
|
78
|
+
continue;
|
|
79
|
+
if (trimmed.startsWith('@'))
|
|
80
|
+
continue; // stacked decorator
|
|
81
|
+
const def = defRe.exec(trimmed);
|
|
82
|
+
if (def)
|
|
83
|
+
handler = def[1];
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
const app = appNames.get(appName);
|
|
87
|
+
const route = makeRouteEntity(input, appName, handler, method.toUpperCase(), path);
|
|
88
|
+
nodes.push(route);
|
|
89
|
+
edges.push(edge(app.id, route.id, EdgeKind.HandlesRoute, {
|
|
90
|
+
method: method.toUpperCase(),
|
|
91
|
+
path,
|
|
92
|
+
handler,
|
|
93
|
+
}));
|
|
94
|
+
edges.push(edge(input.fileNodeId, route.id, EdgeKind.FrameworkDeclares, { subtype: 'route' }));
|
|
95
|
+
}
|
|
96
|
+
return { nodes, edges };
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
function makeAppEntity(input, name, subtype) {
|
|
100
|
+
return {
|
|
101
|
+
id: `framework:fastapi:${subtype}:${input.filePath}#${name}`,
|
|
102
|
+
kind: NodeKind.FrameworkEntity,
|
|
103
|
+
label: name,
|
|
104
|
+
path: input.filePath,
|
|
105
|
+
tags: ['fastapi', subtype],
|
|
106
|
+
data: { framework: 'fastapi', subtype, name },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function makeRouteEntity(input, appName, handler, method, path) {
|
|
110
|
+
return {
|
|
111
|
+
id: `framework:fastapi:route:${input.filePath}#${appName}.${handler}#${method}:${path}`,
|
|
112
|
+
kind: NodeKind.FrameworkEntity,
|
|
113
|
+
label: `${method} ${path}`,
|
|
114
|
+
path: input.filePath,
|
|
115
|
+
tags: ['fastapi', 'route'],
|
|
116
|
+
data: {
|
|
117
|
+
framework: 'fastapi',
|
|
118
|
+
subtype: 'route',
|
|
119
|
+
app: appName,
|
|
120
|
+
handler,
|
|
121
|
+
method,
|
|
122
|
+
path,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function edge(from, to, kind, data) {
|
|
127
|
+
return {
|
|
128
|
+
id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
|
|
129
|
+
from,
|
|
130
|
+
to,
|
|
131
|
+
kind,
|
|
132
|
+
source: FASTAPI_EXTRACTOR_SOURCE,
|
|
133
|
+
...(data ? { data } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IFrameworkExtractor } from '../extractor-api/framework-extractor.js';
|
|
2
|
+
export declare const FASTIFY_EXTRACTOR_SOURCE = "fastify-extractor@v1";
|
|
3
|
+
/**
|
|
4
|
+
* Fastify extractor.
|
|
5
|
+
*
|
|
6
|
+
* Detection model is similar to Express but for Fastify's API:
|
|
7
|
+
* - A variable initialized with `fastify(...)` or `Fastify(...)` becomes a
|
|
8
|
+
* **server** entity.
|
|
9
|
+
* - Subsequent `<server>.get('/path', ...)`, `.post`, etc. become **route**
|
|
10
|
+
* entities. `.route({ method, url })` is also recognised.
|
|
11
|
+
*/
|
|
12
|
+
export declare const fastifyExtractor: IFrameworkExtractor;
|
|
13
|
+
//# sourceMappingURL=fastify-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fastify-extractor.d.ts","sourceRoot":"","sources":["../../src/extractors/fastify-extractor.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,yCAAyC,CAAC;AAEjD,eAAO,MAAM,wBAAwB,yBAAyB,CAAC;AAY/D;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,mBAiE9B,CAAC"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
3
|
+
import * as ts from 'typescript';
|
|
4
|
+
import { EdgeKind, NodeKind } from '@shrkcrft/graph';
|
|
5
|
+
export const FASTIFY_EXTRACTOR_SOURCE = 'fastify-extractor@v1';
|
|
6
|
+
const HTTP_METHOD_NAMES = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'all', 'route']);
|
|
7
|
+
const FAST_FILTER_NEEDLES = [
|
|
8
|
+
"from 'fastify'",
|
|
9
|
+
'from "fastify"',
|
|
10
|
+
'@fastify/',
|
|
11
|
+
'fastify(',
|
|
12
|
+
'Fastify(',
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Fastify extractor.
|
|
16
|
+
*
|
|
17
|
+
* Detection model is similar to Express but for Fastify's API:
|
|
18
|
+
* - A variable initialized with `fastify(...)` or `Fastify(...)` becomes a
|
|
19
|
+
* **server** entity.
|
|
20
|
+
* - Subsequent `<server>.get('/path', ...)`, `.post`, etc. become **route**
|
|
21
|
+
* entities. `.route({ method, url })` is also recognised.
|
|
22
|
+
*/
|
|
23
|
+
export const fastifyExtractor = {
|
|
24
|
+
framework: 'fastify',
|
|
25
|
+
label: 'Fastify',
|
|
26
|
+
fileMatches({ path, content }) {
|
|
27
|
+
if (!/\.(?:t|j)sx?$/.test(path))
|
|
28
|
+
return false;
|
|
29
|
+
for (const needle of FAST_FILTER_NEEDLES) {
|
|
30
|
+
if (content.includes(needle))
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
},
|
|
35
|
+
extract(input) {
|
|
36
|
+
const nodes = [];
|
|
37
|
+
const edges = [];
|
|
38
|
+
const sf = parse(input);
|
|
39
|
+
if (!sf)
|
|
40
|
+
return { nodes, edges };
|
|
41
|
+
const serverNames = new Set();
|
|
42
|
+
const serverNodeByName = new Map();
|
|
43
|
+
const findServers = (node) => {
|
|
44
|
+
if (ts.isVariableStatement(node)) {
|
|
45
|
+
for (const decl of node.declarationList.declarations) {
|
|
46
|
+
if (!ts.isIdentifier(decl.name))
|
|
47
|
+
continue;
|
|
48
|
+
const init = decl.initializer;
|
|
49
|
+
if (!init)
|
|
50
|
+
continue;
|
|
51
|
+
if (isFastifyFactory(init)) {
|
|
52
|
+
const name = decl.name.text;
|
|
53
|
+
serverNames.add(name);
|
|
54
|
+
const entity = makeServerEntity(input, name);
|
|
55
|
+
serverNodeByName.set(name, entity);
|
|
56
|
+
nodes.push(entity);
|
|
57
|
+
edges.push(edge(input.fileNodeId, entity.id, EdgeKind.FrameworkDeclares, { subtype: 'server' }));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
ts.forEachChild(node, findServers);
|
|
62
|
+
};
|
|
63
|
+
ts.forEachChild(sf, findServers);
|
|
64
|
+
const findRoutes = (node) => {
|
|
65
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
66
|
+
const objExpr = node.expression.expression;
|
|
67
|
+
const method = node.expression.name.text;
|
|
68
|
+
if (HTTP_METHOD_NAMES.has(method) &&
|
|
69
|
+
ts.isIdentifier(objExpr) &&
|
|
70
|
+
serverNames.has(objExpr.text)) {
|
|
71
|
+
const route = parseRouteCall(input, objExpr.text, method, node);
|
|
72
|
+
if (route) {
|
|
73
|
+
const serverEntity = serverNodeByName.get(objExpr.text);
|
|
74
|
+
nodes.push(route);
|
|
75
|
+
edges.push(edge(serverEntity.id, route.id, EdgeKind.HandlesRoute, {
|
|
76
|
+
method: route.data?.['method'] ?? '?',
|
|
77
|
+
path: route.data?.['path'] ?? '?',
|
|
78
|
+
}));
|
|
79
|
+
edges.push(edge(input.fileNodeId, route.id, EdgeKind.FrameworkDeclares, { subtype: 'route' }));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
ts.forEachChild(node, findRoutes);
|
|
84
|
+
};
|
|
85
|
+
ts.forEachChild(sf, findRoutes);
|
|
86
|
+
return { nodes, edges };
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
function isFastifyFactory(node) {
|
|
90
|
+
if (!ts.isCallExpression(node))
|
|
91
|
+
return false;
|
|
92
|
+
const callee = node.expression;
|
|
93
|
+
if (ts.isIdentifier(callee)) {
|
|
94
|
+
return callee.text === 'fastify' || callee.text === 'Fastify';
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
function parseRouteCall(input, serverName, method, call) {
|
|
99
|
+
const arg0 = call.arguments[0];
|
|
100
|
+
if (!arg0)
|
|
101
|
+
return undefined;
|
|
102
|
+
if (method === 'route') {
|
|
103
|
+
if (!ts.isObjectLiteralExpression(arg0))
|
|
104
|
+
return undefined;
|
|
105
|
+
let m;
|
|
106
|
+
let p;
|
|
107
|
+
for (const prop of arg0.properties) {
|
|
108
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
|
|
109
|
+
continue;
|
|
110
|
+
const v = prop.initializer;
|
|
111
|
+
if (prop.name.text === 'method' && ts.isStringLiteral(v))
|
|
112
|
+
m = v.text.toUpperCase();
|
|
113
|
+
if ((prop.name.text === 'url' || prop.name.text === 'path') && ts.isStringLiteral(v))
|
|
114
|
+
p = v.text;
|
|
115
|
+
}
|
|
116
|
+
if (!m || !p)
|
|
117
|
+
return undefined;
|
|
118
|
+
return makeRouteEntity(input, serverName, m, p);
|
|
119
|
+
}
|
|
120
|
+
if (!ts.isStringLiteral(arg0))
|
|
121
|
+
return undefined;
|
|
122
|
+
return makeRouteEntity(input, serverName, method.toUpperCase(), arg0.text);
|
|
123
|
+
}
|
|
124
|
+
function makeServerEntity(input, name) {
|
|
125
|
+
return {
|
|
126
|
+
id: `framework:fastify:server:${input.filePath}#${name}`,
|
|
127
|
+
kind: NodeKind.FrameworkEntity,
|
|
128
|
+
label: name,
|
|
129
|
+
path: input.filePath,
|
|
130
|
+
tags: ['fastify', 'server'],
|
|
131
|
+
data: { framework: 'fastify', subtype: 'server', name },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function makeRouteEntity(input, serverName, method, path) {
|
|
135
|
+
return {
|
|
136
|
+
id: `framework:fastify:route:${input.filePath}#${serverName}#${method}:${path}`,
|
|
137
|
+
kind: NodeKind.FrameworkEntity,
|
|
138
|
+
label: `${method} ${path}`,
|
|
139
|
+
path: input.filePath,
|
|
140
|
+
tags: ['fastify', 'route'],
|
|
141
|
+
data: { framework: 'fastify', subtype: 'route', method, path, server: serverName },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function edge(from, to, kind, data) {
|
|
145
|
+
return {
|
|
146
|
+
id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
|
|
147
|
+
from,
|
|
148
|
+
to,
|
|
149
|
+
kind,
|
|
150
|
+
source: FASTIFY_EXTRACTOR_SOURCE,
|
|
151
|
+
...(data ? { data } : {}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function parse(input) {
|
|
155
|
+
const ext = nodePath.extname(input.filePath).toLowerCase();
|
|
156
|
+
const kind = ext === '.tsx' ? ts.ScriptKind.TSX
|
|
157
|
+
: ext === '.jsx' ? ts.ScriptKind.JSX
|
|
158
|
+
: ext === '.js' || ext === '.mjs' || ext === '.cjs' ? ts.ScriptKind.JS
|
|
159
|
+
: ts.ScriptKind.TS;
|
|
160
|
+
try {
|
|
161
|
+
return ts.createSourceFile(input.filePath, input.content, ts.ScriptTarget.Latest, true, kind);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IFrameworkExtractor } from '../extractor-api/framework-extractor.js';
|
|
2
|
+
export declare const FLASK_EXTRACTOR_SOURCE = "flask-extractor@v1";
|
|
3
|
+
/**
|
|
4
|
+
* Flask extractor.
|
|
5
|
+
*
|
|
6
|
+
* Regex-only. Detection model:
|
|
7
|
+
* - `<name> = Flask(...)` → **app** entity.
|
|
8
|
+
* - `<name> = Blueprint('<bp>', __name__, ...)` → **blueprint**
|
|
9
|
+
* entity. The first string argument is captured as the blueprint
|
|
10
|
+
* name.
|
|
11
|
+
* - `@<name>.route('<path>', methods=[...])` immediately preceding a
|
|
12
|
+
* `def <handler>` → **route** entity with method(s) + path +
|
|
13
|
+
* handler. The default method is GET when `methods=` is absent.
|
|
14
|
+
*/
|
|
15
|
+
export declare const flaskExtractor: IFrameworkExtractor;
|
|
16
|
+
//# sourceMappingURL=flask-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flask-extractor.d.ts","sourceRoot":"","sources":["../../src/extractors/flask-extractor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,yCAAyC,CAAC;AAEjD,eAAO,MAAM,sBAAsB,uBAAuB,CAAC;AAW3D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,EAAE,mBA8E5B,CAAC"}
|