@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,164 @@
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 SOLID_EXTRACTOR_SOURCE = 'solid-extractor@v1';
6
+ const SOLID_PRIMITIVE_RE = /^create[A-Z]/;
7
+ const FAST_FILTER_NEEDLES = [
8
+ "from 'solid-js'",
9
+ 'from "solid-js"',
10
+ 'createSignal',
11
+ 'createEffect',
12
+ 'createMemo',
13
+ ];
14
+ /**
15
+ * Solid extractor.
16
+ *
17
+ * Detection (heuristic — no type info):
18
+ * - Top-level `function Component()` / `const Component = (...)` with a
19
+ * name starting with an uppercase letter and a body that produces
20
+ * JSX → component entity.
21
+ * - Uses of `createSignal`, `createEffect`, `createMemo`,
22
+ * `createStore`, etc. → primitive-usage entities, linked to the
23
+ * enclosing component via UsesHook edges.
24
+ *
25
+ * Same shape as the React extractor; lives separately because the
26
+ * detection heuristics (Solid uses primitives instead of hooks) and
27
+ * fast-filter needles differ.
28
+ */
29
+ export const solidExtractor = {
30
+ framework: 'solid',
31
+ label: 'Solid',
32
+ fileMatches({ path, content }) {
33
+ if (!/\.(?:t|j)sx?$/.test(path))
34
+ return false;
35
+ for (const needle of FAST_FILTER_NEEDLES) {
36
+ if (content.includes(needle))
37
+ return true;
38
+ }
39
+ return false;
40
+ },
41
+ extract(input) {
42
+ const nodes = [];
43
+ const edges = [];
44
+ const sf = parse(input);
45
+ if (!sf)
46
+ return { nodes, edges };
47
+ const components = [];
48
+ const primitivesUsed = new Set();
49
+ for (const stmt of sf.statements)
50
+ visitTopLevel(stmt, input, components);
51
+ const enclosingComponent = (n) => {
52
+ for (const c of components)
53
+ if (isAncestor(c.node, n))
54
+ return c.id;
55
+ return undefined;
56
+ };
57
+ const visit = (n) => {
58
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && SOLID_PRIMITIVE_RE.test(n.expression.text)) {
59
+ const name = n.expression.text;
60
+ primitivesUsed.add(name);
61
+ const compId = enclosingComponent(n);
62
+ if (compId) {
63
+ const primId = `framework:solid:primitive-usage:${input.filePath}#${name}`;
64
+ edges.push(edge(compId, primId, EdgeKind.UsesHook, { hook: name }));
65
+ }
66
+ }
67
+ ts.forEachChild(n, visit);
68
+ };
69
+ ts.forEachChild(sf, visit);
70
+ for (const c of components) {
71
+ nodes.push({
72
+ id: c.id,
73
+ kind: NodeKind.FrameworkEntity,
74
+ label: c.name,
75
+ path: input.filePath,
76
+ tags: ['solid', 'component'],
77
+ data: { framework: 'solid', subtype: 'component', name: c.name },
78
+ });
79
+ edges.push(edge(input.fileNodeId, c.id, EdgeKind.FrameworkDeclares, { subtype: 'component' }));
80
+ }
81
+ for (const prim of primitivesUsed) {
82
+ const id = `framework:solid:primitive-usage:${input.filePath}#${prim}`;
83
+ nodes.push({
84
+ id,
85
+ kind: NodeKind.FrameworkEntity,
86
+ label: prim,
87
+ path: input.filePath,
88
+ tags: ['solid', 'primitive-usage'],
89
+ data: { framework: 'solid', subtype: 'primitive-usage', primitive: prim },
90
+ });
91
+ edges.push(edge(input.fileNodeId, id, EdgeKind.FrameworkDeclares, { subtype: 'primitive-usage' }));
92
+ }
93
+ return { nodes, edges };
94
+ },
95
+ };
96
+ function visitTopLevel(stmt, input, out) {
97
+ if (ts.isVariableStatement(stmt)) {
98
+ for (const decl of stmt.declarationList.declarations) {
99
+ if (!ts.isIdentifier(decl.name))
100
+ continue;
101
+ const name = decl.name.text;
102
+ if (!/^[A-Z]/.test(name))
103
+ continue;
104
+ const init = decl.initializer;
105
+ if (!init)
106
+ continue;
107
+ if (containsJsx(init)) {
108
+ out.push({ id: `framework:solid:component:${input.filePath}#${name}`, name, node: init });
109
+ }
110
+ }
111
+ return;
112
+ }
113
+ if (ts.isFunctionDeclaration(stmt) && stmt.name && /^[A-Z]/.test(stmt.name.text)) {
114
+ if (containsJsx(stmt)) {
115
+ out.push({ id: `framework:solid:component:${input.filePath}#${stmt.name.text}`, name: stmt.name.text, node: stmt });
116
+ }
117
+ }
118
+ }
119
+ function containsJsx(node) {
120
+ let found = false;
121
+ const visit = (n) => {
122
+ if (found)
123
+ return;
124
+ if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) {
125
+ found = true;
126
+ return;
127
+ }
128
+ ts.forEachChild(n, visit);
129
+ };
130
+ visit(node);
131
+ return found;
132
+ }
133
+ function isAncestor(ancestor, descendant) {
134
+ let cur = descendant;
135
+ while (cur) {
136
+ if (cur === ancestor)
137
+ return true;
138
+ cur = cur.parent;
139
+ }
140
+ return false;
141
+ }
142
+ function edge(from, to, kind, data) {
143
+ return {
144
+ id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
145
+ from,
146
+ to,
147
+ kind,
148
+ source: SOLID_EXTRACTOR_SOURCE,
149
+ ...(data ? { data } : {}),
150
+ };
151
+ }
152
+ function parse(input) {
153
+ const ext = nodePath.extname(input.filePath).toLowerCase();
154
+ const kind = ext === '.tsx' ? ts.ScriptKind.TSX
155
+ : ext === '.jsx' ? ts.ScriptKind.JSX
156
+ : ext === '.js' || ext === '.mjs' || ext === '.cjs' ? ts.ScriptKind.JS
157
+ : ts.ScriptKind.TS;
158
+ try {
159
+ return ts.createSourceFile(input.filePath, input.content, ts.ScriptTarget.Latest, true, kind);
160
+ }
161
+ catch {
162
+ return undefined;
163
+ }
164
+ }
@@ -0,0 +1,27 @@
1
+ import type { IFrameworkExtractor } from '../extractor-api/framework-extractor.js';
2
+ export declare const SPRING_EXTRACTOR_SOURCE = "spring-extractor@v1";
3
+ /**
4
+ * Spring extractor.
5
+ *
6
+ * Regex-based — Java/Kotlin sources are not AST-parsed. Detection:
7
+ *
8
+ * - **Bean / stereotype classes**: `@Controller`, `@RestController`,
9
+ * `@Service`, `@Repository`, `@Component`, `@Configuration`
10
+ * immediately preceding a `class`/`interface`/`record` declaration
11
+ * → one entity per matched class with subtype = the annotation name
12
+ * lower-cased.
13
+ *
14
+ * - **Routes**: `@RequestMapping`, `@GetMapping`/`@PostMapping`/etc.
15
+ * on a method (or class). Method-level mappings combine with their
16
+ * enclosing class's `@RequestMapping` base path. Method derived
17
+ * from the annotation type (`@GetMapping` → GET); for
18
+ * `@RequestMapping` the optional `method = RequestMethod.GET`
19
+ * argument is captured, otherwise the route is tagged `ANY`.
20
+ *
21
+ * Out of scope:
22
+ * - `@PathVariable`, `@RequestBody`, `@RequestParam` parameter binding.
23
+ * - WebFlux's `RouterFunction` programmatic routes.
24
+ * - Spring Security `SecurityFilterChain` configuration.
25
+ */
26
+ export declare const springExtractor: IFrameworkExtractor;
27
+ //# sourceMappingURL=spring-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spring-extractor.d.ts","sourceRoot":"","sources":["../../src/extractors/spring-extractor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,yCAAyC,CAAC;AAEjD,eAAO,MAAM,uBAAuB,wBAAwB,CAAC;AAmC7D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,eAAe,EAAE,mBAkH7B,CAAC"}
@@ -0,0 +1,279 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { EdgeKind, NodeKind } from '@shrkcrft/graph';
3
+ export const SPRING_EXTRACTOR_SOURCE = 'spring-extractor@v1';
4
+ const STEREOTYPES = new Set([
5
+ 'Controller',
6
+ 'RestController',
7
+ 'Service',
8
+ 'Repository',
9
+ 'Component',
10
+ 'Configuration',
11
+ ]);
12
+ const ROUTE_ANNOTATIONS = new Map([
13
+ ['RequestMapping', undefined],
14
+ ['GetMapping', 'GET'],
15
+ ['PostMapping', 'POST'],
16
+ ['PutMapping', 'PUT'],
17
+ ['DeleteMapping', 'DELETE'],
18
+ ['PatchMapping', 'PATCH'],
19
+ ]);
20
+ const FAST_FILTER_NEEDLES = [
21
+ 'org.springframework',
22
+ '@Controller',
23
+ '@RestController',
24
+ '@Service',
25
+ '@Repository',
26
+ '@Component',
27
+ '@RequestMapping',
28
+ '@GetMapping',
29
+ '@PostMapping',
30
+ '@PutMapping',
31
+ '@DeleteMapping',
32
+ '@PatchMapping',
33
+ ];
34
+ /**
35
+ * Spring extractor.
36
+ *
37
+ * Regex-based — Java/Kotlin sources are not AST-parsed. Detection:
38
+ *
39
+ * - **Bean / stereotype classes**: `@Controller`, `@RestController`,
40
+ * `@Service`, `@Repository`, `@Component`, `@Configuration`
41
+ * immediately preceding a `class`/`interface`/`record` declaration
42
+ * → one entity per matched class with subtype = the annotation name
43
+ * lower-cased.
44
+ *
45
+ * - **Routes**: `@RequestMapping`, `@GetMapping`/`@PostMapping`/etc.
46
+ * on a method (or class). Method-level mappings combine with their
47
+ * enclosing class's `@RequestMapping` base path. Method derived
48
+ * from the annotation type (`@GetMapping` → GET); for
49
+ * `@RequestMapping` the optional `method = RequestMethod.GET`
50
+ * argument is captured, otherwise the route is tagged `ANY`.
51
+ *
52
+ * Out of scope:
53
+ * - `@PathVariable`, `@RequestBody`, `@RequestParam` parameter binding.
54
+ * - WebFlux's `RouterFunction` programmatic routes.
55
+ * - Spring Security `SecurityFilterChain` configuration.
56
+ */
57
+ export const springExtractor = {
58
+ framework: 'spring',
59
+ label: 'Spring',
60
+ fileMatches({ path, content }) {
61
+ if (!path.endsWith('.java') && !path.endsWith('.kt'))
62
+ return false;
63
+ for (const needle of FAST_FILTER_NEEDLES) {
64
+ if (content.includes(needle))
65
+ return true;
66
+ }
67
+ return false;
68
+ },
69
+ extract(input) {
70
+ const nodes = [];
71
+ const edges = [];
72
+ const lines = input.content.split('\n');
73
+ // First pass: locate class/interface/record declarations and the
74
+ // annotations immediately preceding them. We track the most recent
75
+ // class's `@RequestMapping` base path so method-level mappings can
76
+ // combine with it.
77
+ let currentClassEntity;
78
+ let currentClassBasePath = '';
79
+ for (let i = 0; i < lines.length; i += 1) {
80
+ const raw = lines[i];
81
+ const trimmed = raw.trimStart();
82
+ if (trimmed.length === 0)
83
+ continue;
84
+ // Class declaration (column-0, optionally indented inside files
85
+ // — we accept both for Kotlin's `class` at any indent).
86
+ const classMatch = /^(?:public\s+|private\s+|internal\s+|protected\s+|abstract\s+|final\s+|sealed\s+|open\s+|data\s+)*\s*(class|interface|record)\s+([A-Za-z_]\w*)/.exec(trimmed);
87
+ if (classMatch) {
88
+ const className = classMatch[2];
89
+ // Look backwards for annotations on this class.
90
+ const { stereotype, basePath } = scanClassAnnotations(lines, i);
91
+ currentClassBasePath = basePath;
92
+ if (stereotype) {
93
+ const e = makeEntity(input, stereotype.toLowerCase(), className, {
94
+ className,
95
+ stereotype,
96
+ ...(basePath ? { basePath } : {}),
97
+ });
98
+ nodes.push(e);
99
+ edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, {
100
+ subtype: stereotype.toLowerCase(),
101
+ line: i + 1,
102
+ }));
103
+ currentClassEntity = e;
104
+ }
105
+ else if (basePath) {
106
+ // Class without a stereotype but with `@RequestMapping` —
107
+ // still emit a bean entity tagged `mapping` so routes can
108
+ // attach to something.
109
+ const e = makeEntity(input, 'mapping', className, {
110
+ className,
111
+ ...(basePath ? { basePath } : {}),
112
+ });
113
+ nodes.push(e);
114
+ edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, {
115
+ subtype: 'mapping',
116
+ line: i + 1,
117
+ }));
118
+ currentClassEntity = e;
119
+ }
120
+ else {
121
+ currentClassEntity = undefined;
122
+ }
123
+ continue;
124
+ }
125
+ // Method-level route annotation followed by a method declaration.
126
+ const ann = /^@(\w+)(?:\(([^)]*)\))?/.exec(trimmed);
127
+ if (!ann)
128
+ continue;
129
+ const annName = ann[1];
130
+ if (!ROUTE_ANNOTATIONS.has(annName))
131
+ continue;
132
+ const annArgs = ann[2] ?? '';
133
+ // Scan forward through additional annotations to the method/fn
134
+ // declaration line. Skip emitting a route if the annotation is
135
+ // actually class-level (e.g. `@RequestMapping("/users")` above a
136
+ // `class UserController`) — that base path is already captured
137
+ // via `scanClassAnnotations`.
138
+ let handlerName;
139
+ let nextIsClass = false;
140
+ for (let j = i + 1; j < lines.length; j += 1) {
141
+ const t = lines[j].trimStart();
142
+ if (!t)
143
+ continue;
144
+ if (t.startsWith('@'))
145
+ continue;
146
+ if (t.startsWith('//') || t.startsWith('/*'))
147
+ continue;
148
+ if (/^(?:public\s+|private\s+|internal\s+|protected\s+|abstract\s+|final\s+|sealed\s+|open\s+|data\s+)*\s*(class|interface|record)\s+/.test(t)) {
149
+ nextIsClass = true;
150
+ break;
151
+ }
152
+ handlerName = extractMethodName(t);
153
+ break;
154
+ }
155
+ if (nextIsClass)
156
+ continue;
157
+ const finalHandler = handlerName ?? 'handler';
158
+ const methodPath = parseRoutePath(annArgs);
159
+ const methodMethod = ROUTE_ANNOTATIONS.get(annName)
160
+ ?? parseRequestMethod(annArgs)
161
+ ?? 'ANY';
162
+ const finalPath = combinePaths(currentClassBasePath, methodPath);
163
+ const route = makeRouteEntity(input, currentClassEntity?.label ?? '?', finalHandler, methodMethod, finalPath);
164
+ nodes.push(route);
165
+ if (currentClassEntity) {
166
+ edges.push(edge(currentClassEntity.id, route.id, EdgeKind.HandlesRoute, {
167
+ method: methodMethod,
168
+ path: finalPath,
169
+ handler: finalHandler,
170
+ }));
171
+ }
172
+ edges.push(edge(input.fileNodeId, route.id, EdgeKind.FrameworkDeclares, { subtype: 'route', line: i + 1 }));
173
+ }
174
+ return { nodes, edges };
175
+ },
176
+ };
177
+ /**
178
+ * Walk backwards from a class-declaration line to collect its
179
+ * stereotype + `@RequestMapping` base path. Stops at the first
180
+ * non-annotation, non-blank line.
181
+ */
182
+ function scanClassAnnotations(lines, classLineIndex) {
183
+ let stereotype;
184
+ let basePath = '';
185
+ for (let j = classLineIndex - 1; j >= 0; j -= 1) {
186
+ const t = lines[j].trimStart();
187
+ if (!t)
188
+ continue;
189
+ if (!t.startsWith('@'))
190
+ break;
191
+ const m = /^@(\w+)(?:\(([^)]*)\))?/.exec(t);
192
+ if (!m)
193
+ continue;
194
+ const name = m[1];
195
+ if (STEREOTYPES.has(name))
196
+ stereotype = name;
197
+ if (name === 'RequestMapping' || ROUTE_ANNOTATIONS.has(name)) {
198
+ const p = parseRoutePath(m[2] ?? '');
199
+ if (p)
200
+ basePath = p;
201
+ }
202
+ }
203
+ return { stereotype, basePath };
204
+ }
205
+ /**
206
+ * Extract the route path from an annotation argument list. Handles
207
+ * `("/path")`, `(value = "/path")`, `(path = "/path")`.
208
+ */
209
+ function parseRoutePath(args) {
210
+ const m1 = /^\s*"([^"]*)"/.exec(args);
211
+ if (m1)
212
+ return m1[1];
213
+ const m2 = /(?:value|path)\s*=\s*"([^"]*)"/.exec(args);
214
+ if (m2)
215
+ return m2[1];
216
+ return '';
217
+ }
218
+ function parseRequestMethod(args) {
219
+ const m = /method\s*=\s*RequestMethod\.([A-Z]+)/.exec(args);
220
+ if (m)
221
+ return m[1];
222
+ return undefined;
223
+ }
224
+ function extractMethodName(line) {
225
+ // Java: `public ReturnType name(...)` or `ReturnType name(...)`. Kotlin: `fun name(...)`.
226
+ let m = /^(?:public\s+|protected\s+|private\s+|static\s+|final\s+|synchronized\s+)*\s*[\w<>\[\],\s]+?\s+([A-Za-z_]\w*)\s*\(/.exec(line);
227
+ if (m)
228
+ return m[1];
229
+ m = /^(?:public\s+|private\s+|internal\s+|protected\s+|override\s+|suspend\s+|inline\s+)*\s*fun\s+([A-Za-z_]\w*)\s*\(/.exec(line);
230
+ if (m)
231
+ return m[1];
232
+ return undefined;
233
+ }
234
+ function combinePaths(base, leaf) {
235
+ const b = base.replace(/\/$/, '');
236
+ const l = leaf.startsWith('/') ? leaf : '/' + leaf;
237
+ if (!b)
238
+ return l || '/';
239
+ if (!leaf)
240
+ return b || '/';
241
+ return b + l;
242
+ }
243
+ function makeEntity(input, subtype, label, extra) {
244
+ return {
245
+ id: `framework:spring:${subtype}:${input.filePath}#${label}`,
246
+ kind: NodeKind.FrameworkEntity,
247
+ label,
248
+ path: input.filePath,
249
+ tags: ['spring', subtype],
250
+ data: { framework: 'spring', subtype, ...extra },
251
+ };
252
+ }
253
+ function makeRouteEntity(input, className, handler, method, path) {
254
+ return {
255
+ id: `framework:spring:route:${input.filePath}#${className}.${handler}#${method}:${path}`,
256
+ kind: NodeKind.FrameworkEntity,
257
+ label: `${method} ${path}`,
258
+ path: input.filePath,
259
+ tags: ['spring', 'route'],
260
+ data: {
261
+ framework: 'spring',
262
+ subtype: 'route',
263
+ controller: className,
264
+ handler,
265
+ method,
266
+ path,
267
+ },
268
+ };
269
+ }
270
+ function edge(from, to, kind, data) {
271
+ return {
272
+ id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
273
+ from,
274
+ to,
275
+ kind,
276
+ source: SPRING_EXTRACTOR_SOURCE,
277
+ ...(data ? { data } : {}),
278
+ };
279
+ }
@@ -0,0 +1,17 @@
1
+ import type { IFrameworkExtractor } from '../extractor-api/framework-extractor.js';
2
+ export declare const SVELTE_EXTRACTOR_SOURCE = "svelte-extractor@v1";
3
+ /**
4
+ * Svelte extractor.
5
+ *
6
+ * Detection sources:
7
+ * 1. `.svelte` files — always a component. Component name from filename.
8
+ * 2. `.svelte.ts` / `.svelte.js` (Svelte 5 runes in module files) — emits a
9
+ * module-level entity.
10
+ *
11
+ * Lightweight metadata: store usages (`$store` syntax) and Svelte 5 runes
12
+ * (`$state`, `$derived`, etc.) inside the `<script>` block.
13
+ *
14
+ * Out of scope: slot detection, action detection, transition detection.
15
+ */
16
+ export declare const svelteExtractor: IFrameworkExtractor;
17
+ //# sourceMappingURL=svelte-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svelte-extractor.d.ts","sourceRoot":"","sources":["../../src/extractors/svelte-extractor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,yCAAyC,CAAC;AAEjD,eAAO,MAAM,uBAAuB,wBAAwB,CAAC;AAO7D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,EAAE,mBAoC7B,CAAC"}
@@ -0,0 +1,104 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { EdgeKind, NodeKind } from '@shrkcrft/graph';
3
+ export const SVELTE_EXTRACTOR_SOURCE = 'svelte-extractor@v1';
4
+ const STORE_RE = /\$([A-Za-z_][A-Za-z0-9_]*)/g;
5
+ const SCRIPT_OPEN_RE = /<script\b[^>]*>/i;
6
+ const SCRIPT_CLOSE_RE = /<\/script>/i;
7
+ const RUNES_RE = /\$(state|derived|effect|props|bindable|inspect)\s*\(/g;
8
+ /**
9
+ * Svelte extractor.
10
+ *
11
+ * Detection sources:
12
+ * 1. `.svelte` files — always a component. Component name from filename.
13
+ * 2. `.svelte.ts` / `.svelte.js` (Svelte 5 runes in module files) — emits a
14
+ * module-level entity.
15
+ *
16
+ * Lightweight metadata: store usages (`$store` syntax) and Svelte 5 runes
17
+ * (`$state`, `$derived`, etc.) inside the `<script>` block.
18
+ *
19
+ * Out of scope: slot detection, action detection, transition detection.
20
+ */
21
+ export const svelteExtractor = {
22
+ framework: 'svelte',
23
+ label: 'Svelte',
24
+ fileMatches({ path }) {
25
+ return path.endsWith('.svelte') || path.endsWith('.svelte.ts') || path.endsWith('.svelte.js');
26
+ },
27
+ extract(input) {
28
+ const nodes = [];
29
+ const edges = [];
30
+ const filename = input.filePath.split('/').pop();
31
+ const name = filename
32
+ .replace(/\.svelte$/, '')
33
+ .replace(/\.svelte\.(?:t|j)s$/, '');
34
+ const isModule = filename.endsWith('.svelte.ts') || filename.endsWith('.svelte.js');
35
+ const subtype = isModule ? 'module' : 'component';
36
+ const script = isModule ? input.content : extractScriptBlock(input.content);
37
+ const runes = collectRunes(script);
38
+ const stores = collectStoreUsages(script);
39
+ const entity = {
40
+ id: `framework:svelte:${subtype}:${input.filePath}#${name}`,
41
+ kind: NodeKind.FrameworkEntity,
42
+ label: name,
43
+ path: input.filePath,
44
+ tags: ['svelte', subtype],
45
+ data: {
46
+ framework: 'svelte',
47
+ subtype,
48
+ name,
49
+ ...(runes.length > 0 ? { runes } : {}),
50
+ ...(stores.length > 0 ? { stores } : {}),
51
+ },
52
+ };
53
+ nodes.push(entity);
54
+ edges.push(edge(input.fileNodeId, entity.id, EdgeKind.FrameworkDeclares, { subtype }));
55
+ return { nodes, edges };
56
+ },
57
+ };
58
+ function extractScriptBlock(content) {
59
+ const open = content.search(SCRIPT_OPEN_RE);
60
+ if (open < 0)
61
+ return '';
62
+ const after = content.slice(open).indexOf('>');
63
+ if (after < 0)
64
+ return '';
65
+ const start = open + after + 1;
66
+ const closeIdx = content.slice(start).search(SCRIPT_CLOSE_RE);
67
+ if (closeIdx < 0)
68
+ return content.slice(start);
69
+ return content.slice(start, start + closeIdx);
70
+ }
71
+ function collectRunes(script) {
72
+ const found = new Set();
73
+ let m;
74
+ RUNES_RE.lastIndex = 0;
75
+ while ((m = RUNES_RE.exec(script)) !== null)
76
+ found.add('$' + m[1]);
77
+ return [...found].sort();
78
+ }
79
+ function collectStoreUsages(script) {
80
+ const found = new Set();
81
+ let m;
82
+ STORE_RE.lastIndex = 0;
83
+ while ((m = STORE_RE.exec(script)) !== null) {
84
+ const name = m[1];
85
+ // Filter Svelte 5 runes ($state, $derived, etc.) — those aren't stores.
86
+ if (['state', 'derived', 'effect', 'props', 'bindable', 'inspect'].includes(name))
87
+ continue;
88
+ // Filter common JS globals.
89
+ if (name === 'this' || name === 'else')
90
+ continue;
91
+ found.add('$' + name);
92
+ }
93
+ return [...found].sort().slice(0, 20);
94
+ }
95
+ function edge(from, to, kind, data) {
96
+ return {
97
+ id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
98
+ from,
99
+ to,
100
+ kind,
101
+ source: SVELTE_EXTRACTOR_SOURCE,
102
+ ...(data ? { data } : {}),
103
+ };
104
+ }
@@ -0,0 +1,18 @@
1
+ import type { IFrameworkExtractor } from '../extractor-api/framework-extractor.js';
2
+ export declare const VUE_EXTRACTOR_SOURCE = "vue-extractor@v1";
3
+ /**
4
+ * Vue extractor.
5
+ *
6
+ * Detection sources (in order):
7
+ * 1. `.vue` SFC files — always a component, named after the filename.
8
+ * 2. `.ts` / `.tsx` / `.js` / `.jsx` files containing
9
+ * `defineComponent(` or `defineAsyncComponent(` — emits a
10
+ * component entity per call.
11
+ *
12
+ * For SFC files we also detect Composition API hook usages from the
13
+ * `<script>` block via regex (lightweight — no template parsing).
14
+ *
15
+ * Out of scope: template parsing, <style> block detection, scoped slots.
16
+ */
17
+ export declare const vueExtractor: IFrameworkExtractor;
18
+ //# sourceMappingURL=vue-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vue-extractor.d.ts","sourceRoot":"","sources":["../../src/extractors/vue-extractor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,yCAAyC,CAAC;AAEjD,eAAO,MAAM,oBAAoB,qBAAqB,CAAC;AASvD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,EAAE,mBAkB1B,CAAC"}