@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.
Files changed (82) hide show
  1. package/dist/extractor-api/extractor-registry.d.ts +19 -0
  2. package/dist/extractor-api/extractor-registry.d.ts.map +1 -0
  3. package/dist/extractor-api/extractor-registry.js +36 -0
  4. package/dist/extractor-api/framework-extractor.d.ts +48 -0
  5. package/dist/extractor-api/framework-extractor.d.ts.map +1 -0
  6. package/dist/extractor-api/framework-extractor.js +1 -0
  7. package/dist/extractors/angular-extractor.d.ts +18 -0
  8. package/dist/extractors/angular-extractor.d.ts.map +1 -0
  9. package/dist/extractors/angular-extractor.js +175 -0
  10. package/dist/extractors/astro-extractor.d.ts +15 -0
  11. package/dist/extractors/astro-extractor.d.ts.map +1 -0
  12. package/dist/extractors/astro-extractor.js +128 -0
  13. package/dist/extractors/django-extractor.d.ts +24 -0
  14. package/dist/extractors/django-extractor.d.ts.map +1 -0
  15. package/dist/extractors/django-extractor.js +124 -0
  16. package/dist/extractors/express-extractor.d.ts +18 -0
  17. package/dist/extractors/express-extractor.d.ts.map +1 -0
  18. package/dist/extractors/express-extractor.js +193 -0
  19. package/dist/extractors/fastapi-extractor.d.ts +19 -0
  20. package/dist/extractors/fastapi-extractor.d.ts.map +1 -0
  21. package/dist/extractors/fastapi-extractor.js +135 -0
  22. package/dist/extractors/fastify-extractor.d.ts +13 -0
  23. package/dist/extractors/fastify-extractor.d.ts.map +1 -0
  24. package/dist/extractors/fastify-extractor.js +166 -0
  25. package/dist/extractors/flask-extractor.d.ts +16 -0
  26. package/dist/extractors/flask-extractor.d.ts.map +1 -0
  27. package/dist/extractors/flask-extractor.js +142 -0
  28. package/dist/extractors/flutter-extractor.d.ts +26 -0
  29. package/dist/extractors/flutter-extractor.d.ts.map +1 -0
  30. package/dist/extractors/flutter-extractor.js +137 -0
  31. package/dist/extractors/graphql-extractor.d.ts +27 -0
  32. package/dist/extractors/graphql-extractor.d.ts.map +1 -0
  33. package/dist/extractors/graphql-extractor.js +141 -0
  34. package/dist/extractors/laravel-extractor.d.ts +22 -0
  35. package/dist/extractors/laravel-extractor.d.ts.map +1 -0
  36. package/dist/extractors/laravel-extractor.js +208 -0
  37. package/dist/extractors/nestjs-extractor.d.ts +18 -0
  38. package/dist/extractors/nestjs-extractor.d.ts.map +1 -0
  39. package/dist/extractors/nestjs-extractor.js +222 -0
  40. package/dist/extractors/nextjs-extractor.d.ts +19 -0
  41. package/dist/extractors/nextjs-extractor.d.ts.map +1 -0
  42. package/dist/extractors/nextjs-extractor.js +175 -0
  43. package/dist/extractors/phoenix-extractor.d.ts +28 -0
  44. package/dist/extractors/phoenix-extractor.d.ts.map +1 -0
  45. package/dist/extractors/phoenix-extractor.js +212 -0
  46. package/dist/extractors/rails-extractor.d.ts +25 -0
  47. package/dist/extractors/rails-extractor.d.ts.map +1 -0
  48. package/dist/extractors/rails-extractor.js +180 -0
  49. package/dist/extractors/react-extractor.d.ts +19 -0
  50. package/dist/extractors/react-extractor.d.ts.map +1 -0
  51. package/dist/extractors/react-extractor.js +209 -0
  52. package/dist/extractors/solid-extractor.d.ts +19 -0
  53. package/dist/extractors/solid-extractor.d.ts.map +1 -0
  54. package/dist/extractors/solid-extractor.js +164 -0
  55. package/dist/extractors/spring-extractor.d.ts +27 -0
  56. package/dist/extractors/spring-extractor.d.ts.map +1 -0
  57. package/dist/extractors/spring-extractor.js +279 -0
  58. package/dist/extractors/svelte-extractor.d.ts +17 -0
  59. package/dist/extractors/svelte-extractor.d.ts.map +1 -0
  60. package/dist/extractors/svelte-extractor.js +104 -0
  61. package/dist/extractors/vue-extractor.d.ts +18 -0
  62. package/dist/extractors/vue-extractor.d.ts.map +1 -0
  63. package/dist/extractors/vue-extractor.js +125 -0
  64. package/dist/index.d.ts +27 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +26 -0
  67. package/dist/query/framework-query-api.d.ts +39 -0
  68. package/dist/query/framework-query-api.d.ts.map +1 -0
  69. package/dist/query/framework-query-api.js +99 -0
  70. package/dist/runner/load-pack-extractors.d.ts +36 -0
  71. package/dist/runner/load-pack-extractors.d.ts.map +1 -0
  72. package/dist/runner/load-pack-extractors.js +87 -0
  73. package/dist/runner/run-extractors.d.ts +29 -0
  74. package/dist/runner/run-extractors.d.ts.map +1 -0
  75. package/dist/runner/run-extractors.js +144 -0
  76. package/dist/schema/framework-schema.d.ts +36 -0
  77. package/dist/schema/framework-schema.d.ts.map +1 -0
  78. package/dist/schema/framework-schema.js +1 -0
  79. package/dist/store/framework-store.d.ts +17 -0
  80. package/dist/store/framework-store.d.ts.map +1 -0
  81. package/dist/store/framework-store.js +138 -0
  82. 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"}