@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,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"}
|