@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,142 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { EdgeKind, NodeKind } from '@shrkcrft/graph';
|
|
3
|
+
export const FLASK_EXTRACTOR_SOURCE = 'flask-extractor@v1';
|
|
4
|
+
const FAST_FILTER_NEEDLES = [
|
|
5
|
+
'from flask',
|
|
6
|
+
'Flask(__name__)',
|
|
7
|
+
'Flask(name=',
|
|
8
|
+
'Blueprint(',
|
|
9
|
+
'@app.route',
|
|
10
|
+
'@blueprint.route',
|
|
11
|
+
];
|
|
12
|
+
/**
|
|
13
|
+
* Flask extractor.
|
|
14
|
+
*
|
|
15
|
+
* Regex-only. Detection model:
|
|
16
|
+
* - `<name> = Flask(...)` → **app** entity.
|
|
17
|
+
* - `<name> = Blueprint('<bp>', __name__, ...)` → **blueprint**
|
|
18
|
+
* entity. The first string argument is captured as the blueprint
|
|
19
|
+
* name.
|
|
20
|
+
* - `@<name>.route('<path>', methods=[...])` immediately preceding a
|
|
21
|
+
* `def <handler>` → **route** entity with method(s) + path +
|
|
22
|
+
* handler. The default method is GET when `methods=` is absent.
|
|
23
|
+
*/
|
|
24
|
+
export const flaskExtractor = {
|
|
25
|
+
framework: 'flask',
|
|
26
|
+
label: 'Flask',
|
|
27
|
+
fileMatches({ path, content }) {
|
|
28
|
+
if (!path.endsWith('.py'))
|
|
29
|
+
return false;
|
|
30
|
+
for (const needle of FAST_FILTER_NEEDLES) {
|
|
31
|
+
if (content.includes(needle))
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
},
|
|
36
|
+
extract(input) {
|
|
37
|
+
const nodes = [];
|
|
38
|
+
const edges = [];
|
|
39
|
+
const lines = input.content.split('\n');
|
|
40
|
+
const appNames = new Map();
|
|
41
|
+
const flaskRe = /^([A-Za-z_]\w*)\s*=\s*Flask\s*\(/;
|
|
42
|
+
const bpRe = /^([A-Za-z_]\w*)\s*=\s*Blueprint\s*\(\s*['"]([^'"]+)['"]/;
|
|
43
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
44
|
+
const raw = lines[i];
|
|
45
|
+
let m = flaskRe.exec(raw);
|
|
46
|
+
if (m) {
|
|
47
|
+
const name = m[1];
|
|
48
|
+
const e = makeEntity(input, 'app', name, { name });
|
|
49
|
+
appNames.set(name, e);
|
|
50
|
+
nodes.push(e);
|
|
51
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'app', line: i + 1 }));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
m = bpRe.exec(raw);
|
|
55
|
+
if (m) {
|
|
56
|
+
const localName = m[1];
|
|
57
|
+
const bpName = m[2];
|
|
58
|
+
const e = makeEntity(input, 'blueprint', bpName, { localName, name: bpName });
|
|
59
|
+
appNames.set(localName, e);
|
|
60
|
+
nodes.push(e);
|
|
61
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'blueprint', line: i + 1 }));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (appNames.size === 0)
|
|
65
|
+
return { nodes, edges };
|
|
66
|
+
// Routes: `@<appOrBlueprint>.route('<path>', methods=[...])` then a `def`.
|
|
67
|
+
const routeRe = /^@([A-Za-z_]\w*)\.route\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*methods\s*=\s*\[([^\]]+)\])?/;
|
|
68
|
+
const defRe = /^(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/;
|
|
69
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
70
|
+
const m = routeRe.exec(lines[i]);
|
|
71
|
+
if (!m)
|
|
72
|
+
continue;
|
|
73
|
+
const target = m[1];
|
|
74
|
+
const path = m[2];
|
|
75
|
+
const methodsRaw = m[3];
|
|
76
|
+
const methods = methodsRaw
|
|
77
|
+
? methodsRaw.split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '').toUpperCase()).filter(Boolean)
|
|
78
|
+
: ['GET'];
|
|
79
|
+
if (!appNames.has(target))
|
|
80
|
+
continue;
|
|
81
|
+
// Find handler name on a subsequent non-decorator line.
|
|
82
|
+
let handler = 'handler';
|
|
83
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
84
|
+
const t = lines[j].trimStart();
|
|
85
|
+
if (!t)
|
|
86
|
+
continue;
|
|
87
|
+
if (t.startsWith('@'))
|
|
88
|
+
continue;
|
|
89
|
+
const def = defRe.exec(t);
|
|
90
|
+
if (def)
|
|
91
|
+
handler = def[1];
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
// Emit one route entity per HTTP method (matches FastAPI's per-method shape).
|
|
95
|
+
const app = appNames.get(target);
|
|
96
|
+
for (const method of methods) {
|
|
97
|
+
const e = makeRouteEntity(input, target, handler, method, path);
|
|
98
|
+
nodes.push(e);
|
|
99
|
+
edges.push(edge(app.id, e.id, EdgeKind.HandlesRoute, { method, path, handler }));
|
|
100
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'route', line: i + 1 }));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { nodes, edges };
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
function makeEntity(input, subtype, label, extra) {
|
|
107
|
+
return {
|
|
108
|
+
id: `framework:flask:${subtype}:${input.filePath}#${label}`,
|
|
109
|
+
kind: NodeKind.FrameworkEntity,
|
|
110
|
+
label,
|
|
111
|
+
path: input.filePath,
|
|
112
|
+
tags: ['flask', subtype],
|
|
113
|
+
data: { framework: 'flask', subtype, ...extra },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function makeRouteEntity(input, appName, handler, method, path) {
|
|
117
|
+
return {
|
|
118
|
+
id: `framework:flask:route:${input.filePath}#${appName}.${handler}#${method}:${path}`,
|
|
119
|
+
kind: NodeKind.FrameworkEntity,
|
|
120
|
+
label: `${method} ${path}`,
|
|
121
|
+
path: input.filePath,
|
|
122
|
+
tags: ['flask', 'route'],
|
|
123
|
+
data: {
|
|
124
|
+
framework: 'flask',
|
|
125
|
+
subtype: 'route',
|
|
126
|
+
app: appName,
|
|
127
|
+
handler,
|
|
128
|
+
method,
|
|
129
|
+
path,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function edge(from, to, kind, data) {
|
|
134
|
+
return {
|
|
135
|
+
id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
|
|
136
|
+
from,
|
|
137
|
+
to,
|
|
138
|
+
kind,
|
|
139
|
+
source: FLASK_EXTRACTOR_SOURCE,
|
|
140
|
+
...(data ? { data } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { IFrameworkExtractor } from '../extractor-api/framework-extractor.js';
|
|
2
|
+
export declare const FLUTTER_EXTRACTOR_SOURCE = "flutter-extractor@v1";
|
|
3
|
+
/**
|
|
4
|
+
* Flutter framework extractor.
|
|
5
|
+
*
|
|
6
|
+
* Regex-only — Dart source isn't AST-parsed. Detection:
|
|
7
|
+
*
|
|
8
|
+
* - **Widget**: `class X extends StatelessWidget` / `StatefulWidget` /
|
|
9
|
+
* `ConsumerWidget` / `HookWidget` / `HookConsumerWidget`. Emits
|
|
10
|
+
* one entity per matched class.
|
|
11
|
+
*
|
|
12
|
+
* - **State**: `class _XState extends State<X>` →
|
|
13
|
+
* state entity, linked to its parent widget via `UsesHook` edges
|
|
14
|
+
* (re-using the edge kind for "this state belongs to that widget").
|
|
15
|
+
*
|
|
16
|
+
* - **Notifier**: `class X extends ChangeNotifier` /
|
|
17
|
+
* `class X with ChangeNotifier` → notifier entity. Common for
|
|
18
|
+
* Provider / Riverpod-style state.
|
|
19
|
+
*
|
|
20
|
+
* Out of scope:
|
|
21
|
+
* - Widget tree inspection (the `build()` body).
|
|
22
|
+
* - InheritedWidget chains.
|
|
23
|
+
* - Riverpod provider declarations.
|
|
24
|
+
*/
|
|
25
|
+
export declare const flutterExtractor: IFrameworkExtractor;
|
|
26
|
+
//# sourceMappingURL=flutter-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flutter-extractor.d.ts","sourceRoot":"","sources":["../../src/extractors/flutter-extractor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,yCAAyC,CAAC;AAEjD,eAAO,MAAM,wBAAwB,yBAAyB,CAAC;AAmB/D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,gBAAgB,EAAE,mBA8E9B,CAAC"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { EdgeKind, NodeKind } from '@shrkcrft/graph';
|
|
3
|
+
export const FLUTTER_EXTRACTOR_SOURCE = 'flutter-extractor@v1';
|
|
4
|
+
const WIDGET_BASES = new Set([
|
|
5
|
+
'StatelessWidget',
|
|
6
|
+
'StatefulWidget',
|
|
7
|
+
'ConsumerWidget',
|
|
8
|
+
'HookWidget',
|
|
9
|
+
'HookConsumerWidget',
|
|
10
|
+
]);
|
|
11
|
+
const FAST_FILTER_NEEDLES = [
|
|
12
|
+
"package:flutter/",
|
|
13
|
+
'extends StatelessWidget',
|
|
14
|
+
'extends StatefulWidget',
|
|
15
|
+
'extends State<',
|
|
16
|
+
'ChangeNotifier',
|
|
17
|
+
'ConsumerWidget',
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Flutter framework extractor.
|
|
21
|
+
*
|
|
22
|
+
* Regex-only — Dart source isn't AST-parsed. Detection:
|
|
23
|
+
*
|
|
24
|
+
* - **Widget**: `class X extends StatelessWidget` / `StatefulWidget` /
|
|
25
|
+
* `ConsumerWidget` / `HookWidget` / `HookConsumerWidget`. Emits
|
|
26
|
+
* one entity per matched class.
|
|
27
|
+
*
|
|
28
|
+
* - **State**: `class _XState extends State<X>` →
|
|
29
|
+
* state entity, linked to its parent widget via `UsesHook` edges
|
|
30
|
+
* (re-using the edge kind for "this state belongs to that widget").
|
|
31
|
+
*
|
|
32
|
+
* - **Notifier**: `class X extends ChangeNotifier` /
|
|
33
|
+
* `class X with ChangeNotifier` → notifier entity. Common for
|
|
34
|
+
* Provider / Riverpod-style state.
|
|
35
|
+
*
|
|
36
|
+
* Out of scope:
|
|
37
|
+
* - Widget tree inspection (the `build()` body).
|
|
38
|
+
* - InheritedWidget chains.
|
|
39
|
+
* - Riverpod provider declarations.
|
|
40
|
+
*/
|
|
41
|
+
export const flutterExtractor = {
|
|
42
|
+
framework: 'flutter',
|
|
43
|
+
label: 'Flutter',
|
|
44
|
+
fileMatches({ path, content }) {
|
|
45
|
+
if (!path.endsWith('.dart'))
|
|
46
|
+
return false;
|
|
47
|
+
for (const needle of FAST_FILTER_NEEDLES) {
|
|
48
|
+
if (content.includes(needle))
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
},
|
|
53
|
+
extract(input) {
|
|
54
|
+
const nodes = [];
|
|
55
|
+
const edges = [];
|
|
56
|
+
const lines = input.content.split('\n');
|
|
57
|
+
// Track widget classes so a follow-up `_XState extends State<X>`
|
|
58
|
+
// can link back via the X parameter.
|
|
59
|
+
const widgetEntities = new Map();
|
|
60
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
61
|
+
const raw = lines[i];
|
|
62
|
+
if (raw.startsWith(' ') || raw.startsWith('\t'))
|
|
63
|
+
continue;
|
|
64
|
+
const trimmed = raw.trimStart();
|
|
65
|
+
// Widget classes.
|
|
66
|
+
let m = /^class\s+([A-Za-z_]\w*)\s+extends\s+([A-Za-z_]\w*)/.exec(trimmed);
|
|
67
|
+
if (m) {
|
|
68
|
+
const className = m[1];
|
|
69
|
+
const baseClass = m[2];
|
|
70
|
+
if (WIDGET_BASES.has(baseClass)) {
|
|
71
|
+
const e = makeEntity(input, 'widget', className, {
|
|
72
|
+
className,
|
|
73
|
+
baseClass,
|
|
74
|
+
stateful: baseClass === 'StatefulWidget',
|
|
75
|
+
});
|
|
76
|
+
widgetEntities.set(className, e);
|
|
77
|
+
nodes.push(e);
|
|
78
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'widget', line: i + 1 }));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (baseClass === 'ChangeNotifier') {
|
|
82
|
+
const e = makeEntity(input, 'notifier', className, { className, baseClass });
|
|
83
|
+
nodes.push(e);
|
|
84
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'notifier', line: i + 1 }));
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// State<Widget> classes.
|
|
89
|
+
m = /^class\s+([A-Za-z_]\w*)\s+extends\s+State\s*<\s*([A-Za-z_]\w*)\s*>/.exec(trimmed);
|
|
90
|
+
if (m) {
|
|
91
|
+
const stateName = m[1];
|
|
92
|
+
const widgetName = m[2];
|
|
93
|
+
const e = makeEntity(input, 'state', stateName, {
|
|
94
|
+
stateName,
|
|
95
|
+
widget: widgetName,
|
|
96
|
+
});
|
|
97
|
+
nodes.push(e);
|
|
98
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'state', line: i + 1 }));
|
|
99
|
+
// Wire state → widget if we've seen the widget already.
|
|
100
|
+
const widget = widgetEntities.get(widgetName);
|
|
101
|
+
if (widget) {
|
|
102
|
+
edges.push(edge(widget.id, e.id, EdgeKind.UsesHook, { kind: 'state', widget: widgetName }));
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// Mixin form: `class X extends Y with ChangeNotifier`
|
|
107
|
+
m = /^class\s+([A-Za-z_]\w*)\s+(?:extends\s+[A-Za-z_]\w*\s+)?with\s+([^{]+)/.exec(trimmed);
|
|
108
|
+
if (m && m[2].split(',').map((s) => s.trim()).includes('ChangeNotifier')) {
|
|
109
|
+
const className = m[1];
|
|
110
|
+
const e = makeEntity(input, 'notifier', className, { className, baseClass: 'with ChangeNotifier' });
|
|
111
|
+
nodes.push(e);
|
|
112
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'notifier', line: i + 1 }));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { nodes, edges };
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
function makeEntity(input, subtype, label, extra) {
|
|
119
|
+
return {
|
|
120
|
+
id: `framework:flutter:${subtype}:${input.filePath}#${label}`,
|
|
121
|
+
kind: NodeKind.FrameworkEntity,
|
|
122
|
+
label,
|
|
123
|
+
path: input.filePath,
|
|
124
|
+
tags: ['flutter', subtype],
|
|
125
|
+
data: { framework: 'flutter', subtype, ...extra },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function edge(from, to, kind, data) {
|
|
129
|
+
return {
|
|
130
|
+
id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
|
|
131
|
+
from,
|
|
132
|
+
to,
|
|
133
|
+
kind,
|
|
134
|
+
source: FLUTTER_EXTRACTOR_SOURCE,
|
|
135
|
+
...(data ? { data } : {}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IFrameworkExtractor } from '../extractor-api/framework-extractor.js';
|
|
2
|
+
export declare const GRAPHQL_EXTRACTOR_SOURCE = "graphql-extractor@v1";
|
|
3
|
+
/**
|
|
4
|
+
* GraphQL schema extractor.
|
|
5
|
+
*
|
|
6
|
+
* Parses `.graphql` / `.gql` SDL files. Detected:
|
|
7
|
+
* - `type Name { … }` → type
|
|
8
|
+
* - `interface Name { … }` → interface
|
|
9
|
+
* - `enum Name { … }` → enum
|
|
10
|
+
* - `input Name { … }` → input
|
|
11
|
+
* - `union Name = A | B` → union
|
|
12
|
+
* - `scalar Name` → scalar
|
|
13
|
+
* - `directive @name on …` → directive
|
|
14
|
+
*
|
|
15
|
+
* The three root types — `Query`, `Mutation`, `Subscription` — get one
|
|
16
|
+
* extra **field** entity per declared field. Each is wired back to the
|
|
17
|
+
* parent type via `HandlesRoute` so the dashboard's Routes panel can
|
|
18
|
+
* surface GraphQL operations alongside HTTP routes.
|
|
19
|
+
*
|
|
20
|
+
* Out of scope:
|
|
21
|
+
* - `schema { query: ..., mutation: ... }` aliasing.
|
|
22
|
+
* - `extend type X { … }` (the extender re-declares).
|
|
23
|
+
* - Field arguments / nested complex types.
|
|
24
|
+
* - GraphQL-in-strings (e.g. JS `gql\`type Foo { ... }\``).
|
|
25
|
+
*/
|
|
26
|
+
export declare const graphqlExtractor: IFrameworkExtractor;
|
|
27
|
+
//# sourceMappingURL=graphql-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphql-extractor.d.ts","sourceRoot":"","sources":["../../src/extractors/graphql-extractor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,yCAAyC,CAAC;AAEjD,eAAO,MAAM,wBAAwB,yBAAyB,CAAC;AAI/D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,gBAAgB,EAAE,mBAiF9B,CAAC"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { EdgeKind, NodeKind } from '@shrkcrft/graph';
|
|
3
|
+
export const GRAPHQL_EXTRACTOR_SOURCE = 'graphql-extractor@v1';
|
|
4
|
+
const ROOT_TYPE_NAMES = new Set(['Query', 'Mutation', 'Subscription']);
|
|
5
|
+
/**
|
|
6
|
+
* GraphQL schema extractor.
|
|
7
|
+
*
|
|
8
|
+
* Parses `.graphql` / `.gql` SDL files. Detected:
|
|
9
|
+
* - `type Name { … }` → type
|
|
10
|
+
* - `interface Name { … }` → interface
|
|
11
|
+
* - `enum Name { … }` → enum
|
|
12
|
+
* - `input Name { … }` → input
|
|
13
|
+
* - `union Name = A | B` → union
|
|
14
|
+
* - `scalar Name` → scalar
|
|
15
|
+
* - `directive @name on …` → directive
|
|
16
|
+
*
|
|
17
|
+
* The three root types — `Query`, `Mutation`, `Subscription` — get one
|
|
18
|
+
* extra **field** entity per declared field. Each is wired back to the
|
|
19
|
+
* parent type via `HandlesRoute` so the dashboard's Routes panel can
|
|
20
|
+
* surface GraphQL operations alongside HTTP routes.
|
|
21
|
+
*
|
|
22
|
+
* Out of scope:
|
|
23
|
+
* - `schema { query: ..., mutation: ... }` aliasing.
|
|
24
|
+
* - `extend type X { … }` (the extender re-declares).
|
|
25
|
+
* - Field arguments / nested complex types.
|
|
26
|
+
* - GraphQL-in-strings (e.g. JS `gql\`type Foo { ... }\``).
|
|
27
|
+
*/
|
|
28
|
+
export const graphqlExtractor = {
|
|
29
|
+
framework: 'graphql',
|
|
30
|
+
label: 'GraphQL',
|
|
31
|
+
fileMatches({ path }) {
|
|
32
|
+
return path.endsWith('.graphql') || path.endsWith('.gql');
|
|
33
|
+
},
|
|
34
|
+
extract(input) {
|
|
35
|
+
const nodes = [];
|
|
36
|
+
const edges = [];
|
|
37
|
+
const lines = input.content.split('\n');
|
|
38
|
+
// Walk line by line, tracking the open root-type block (so each
|
|
39
|
+
// line inside it can be parsed as a field). Comments (`#`) are
|
|
40
|
+
// stripped.
|
|
41
|
+
let currentRootType;
|
|
42
|
+
let currentRootKind;
|
|
43
|
+
let braceDepth = 0;
|
|
44
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
45
|
+
const raw = lines[i];
|
|
46
|
+
const trimmed = raw.replace(/#.*$/, '').trim();
|
|
47
|
+
if (!trimmed)
|
|
48
|
+
continue;
|
|
49
|
+
// Track brace depth to know when we exit a block.
|
|
50
|
+
// (We open the block on the same line as the declaration and
|
|
51
|
+
// close it when '}' appears.)
|
|
52
|
+
if (currentRootType && trimmed.includes('}')) {
|
|
53
|
+
braceDepth -= (trimmed.match(/\}/g) ?? []).length;
|
|
54
|
+
if (braceDepth <= 0) {
|
|
55
|
+
currentRootType = undefined;
|
|
56
|
+
currentRootKind = undefined;
|
|
57
|
+
braceDepth = 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Type-shape declarations.
|
|
61
|
+
let m = /^(type|interface|enum|input|union|scalar|directive)\s+(@?[A-Za-z_][\w]*)/.exec(trimmed);
|
|
62
|
+
if (m) {
|
|
63
|
+
const kind = m[1];
|
|
64
|
+
const rawName = m[2];
|
|
65
|
+
const name = rawName.startsWith('@') ? rawName.slice(1) : rawName;
|
|
66
|
+
const entity = makeEntity(input, kind, name, { kind, name });
|
|
67
|
+
nodes.push(entity);
|
|
68
|
+
edges.push(edge(input.fileNodeId, entity.id, EdgeKind.FrameworkDeclares, { subtype: kind, line: i + 1 }));
|
|
69
|
+
// Open a root-type block when the declaration opens with `{`.
|
|
70
|
+
if ((kind === 'type' || kind === 'interface') && trimmed.includes('{')) {
|
|
71
|
+
braceDepth += (trimmed.match(/\{/g) ?? []).length;
|
|
72
|
+
braceDepth -= (trimmed.match(/\}/g) ?? []).length;
|
|
73
|
+
if (ROOT_TYPE_NAMES.has(name)) {
|
|
74
|
+
currentRootType = entity;
|
|
75
|
+
currentRootKind = name;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// Inside a Query / Mutation / Subscription block — capture fields.
|
|
81
|
+
if (currentRootType && currentRootKind) {
|
|
82
|
+
const fieldMatch = /^([A-Za-z_][\w]*)\s*(?:\(([^)]*)\))?\s*:\s*([\w\[\]!]+)/.exec(trimmed);
|
|
83
|
+
if (fieldMatch) {
|
|
84
|
+
const fieldName = fieldMatch[1];
|
|
85
|
+
const argString = fieldMatch[2] ?? '';
|
|
86
|
+
const returnType = fieldMatch[3];
|
|
87
|
+
const fieldEntity = makeFieldEntity(input, currentRootKind, fieldName, returnType, argString);
|
|
88
|
+
nodes.push(fieldEntity);
|
|
89
|
+
edges.push(edge(currentRootType.id, fieldEntity.id, EdgeKind.HandlesRoute, {
|
|
90
|
+
operation: currentRootKind.toLowerCase(),
|
|
91
|
+
field: fieldName,
|
|
92
|
+
}));
|
|
93
|
+
edges.push(edge(input.fileNodeId, fieldEntity.id, EdgeKind.FrameworkDeclares, {
|
|
94
|
+
subtype: 'operation',
|
|
95
|
+
line: i + 1,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
// Continue tracking brace depth for nested blocks.
|
|
99
|
+
braceDepth += (trimmed.match(/\{/g) ?? []).length;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { nodes, edges };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
function makeEntity(input, subtype, label, extra) {
|
|
106
|
+
return {
|
|
107
|
+
id: `framework:graphql:${subtype}:${input.filePath}#${label}`,
|
|
108
|
+
kind: NodeKind.FrameworkEntity,
|
|
109
|
+
label,
|
|
110
|
+
path: input.filePath,
|
|
111
|
+
tags: ['graphql', subtype],
|
|
112
|
+
data: { framework: 'graphql', subtype, ...extra },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function makeFieldEntity(input, rootKind, fieldName, returnType, args) {
|
|
116
|
+
return {
|
|
117
|
+
id: `framework:graphql:operation:${input.filePath}#${rootKind}.${fieldName}`,
|
|
118
|
+
kind: NodeKind.FrameworkEntity,
|
|
119
|
+
label: `${rootKind.toLowerCase()} ${fieldName}`,
|
|
120
|
+
path: input.filePath,
|
|
121
|
+
tags: ['graphql', 'operation', rootKind.toLowerCase()],
|
|
122
|
+
data: {
|
|
123
|
+
framework: 'graphql',
|
|
124
|
+
subtype: 'operation',
|
|
125
|
+
operation: rootKind.toLowerCase(),
|
|
126
|
+
field: fieldName,
|
|
127
|
+
returnType,
|
|
128
|
+
...(args ? { args } : {}),
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function edge(from, to, kind, data) {
|
|
133
|
+
return {
|
|
134
|
+
id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
|
|
135
|
+
from,
|
|
136
|
+
to,
|
|
137
|
+
kind,
|
|
138
|
+
source: GRAPHQL_EXTRACTOR_SOURCE,
|
|
139
|
+
...(data ? { data } : {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IFrameworkExtractor } from '../extractor-api/framework-extractor.js';
|
|
2
|
+
export declare const LARAVEL_EXTRACTOR_SOURCE = "laravel-extractor@v1";
|
|
3
|
+
/**
|
|
4
|
+
* Laravel framework extractor.
|
|
5
|
+
*
|
|
6
|
+
* Regex-only. Detection:
|
|
7
|
+
* - Class extending `Controller` / `BaseController` / `RestController` → controller.
|
|
8
|
+
* - Class extending `Model` / `Authenticatable` / `Pivot` → model.
|
|
9
|
+
* - Class extending `JsonResource` / `ResourceCollection` → resource.
|
|
10
|
+
* - `Route::get('/path', [Controller::class, 'action'])` (and other verbs) → route.
|
|
11
|
+
* - `Route::resource('users', UserController::class)` → route (RESOURCE).
|
|
12
|
+
*
|
|
13
|
+
* Inside controllers, every `public function name(...)` becomes an
|
|
14
|
+
* action entity wired back to the controller via `HandlesRoute`.
|
|
15
|
+
*
|
|
16
|
+
* Out of scope:
|
|
17
|
+
* - Route groups (`Route::middleware(...)->group(function () { … })`)
|
|
18
|
+
* - Model relations (hasMany, belongsTo, …).
|
|
19
|
+
* - Blade templates.
|
|
20
|
+
*/
|
|
21
|
+
export declare const laravelExtractor: IFrameworkExtractor;
|
|
22
|
+
//# sourceMappingURL=laravel-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"laravel-extractor.d.ts","sourceRoot":"","sources":["../../src/extractors/laravel-extractor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,yCAAyC,CAAC;AAEjD,eAAO,MAAM,wBAAwB,yBAAyB,CAAC;AAa/D;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,gBAAgB,EAAE,mBAuI9B,CAAC"}
|