@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,19 @@
|
|
|
1
|
+
import type { IFrameworkExtractor } from './framework-extractor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Registry of framework extractors. The runner registers the built-in
|
|
4
|
+
* extractors at startup; pack-contributed extractors can register
|
|
5
|
+
* later via `register(...)`.
|
|
6
|
+
*/
|
|
7
|
+
export declare class FrameworkExtractorRegistry {
|
|
8
|
+
private readonly byName;
|
|
9
|
+
register(extractor: IFrameworkExtractor): void;
|
|
10
|
+
list(): readonly IFrameworkExtractor[];
|
|
11
|
+
applicable(file: {
|
|
12
|
+
path: string;
|
|
13
|
+
content: string;
|
|
14
|
+
}): readonly IFrameworkExtractor[];
|
|
15
|
+
has(framework: string): boolean;
|
|
16
|
+
get(framework: string): IFrameworkExtractor | undefined;
|
|
17
|
+
size(): number;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=extractor-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extractor-registry.d.ts","sourceRoot":"","sources":["../../src/extractor-api/extractor-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAEpE;;;;GAIG;AACH,qBAAa,0BAA0B;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IAEjE,QAAQ,CAAC,SAAS,EAAE,mBAAmB,GAAG,IAAI;IAO9C,IAAI,IAAI,SAAS,mBAAmB,EAAE;IAItC,UAAU,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,mBAAmB,EAAE;IAUnF,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAI/B,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAIvD,IAAI,IAAI,MAAM;CAGf"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of framework extractors. The runner registers the built-in
|
|
3
|
+
* extractors at startup; pack-contributed extractors can register
|
|
4
|
+
* later via `register(...)`.
|
|
5
|
+
*/
|
|
6
|
+
export class FrameworkExtractorRegistry {
|
|
7
|
+
byName = new Map();
|
|
8
|
+
register(extractor) {
|
|
9
|
+
if (this.byName.has(extractor.framework)) {
|
|
10
|
+
throw new Error(`framework extractor already registered: ${extractor.framework}`);
|
|
11
|
+
}
|
|
12
|
+
this.byName.set(extractor.framework, extractor);
|
|
13
|
+
}
|
|
14
|
+
list() {
|
|
15
|
+
return [...this.byName.values()].sort((a, b) => a.framework.localeCompare(b.framework));
|
|
16
|
+
}
|
|
17
|
+
applicable(file) {
|
|
18
|
+
return this.list().filter((e) => {
|
|
19
|
+
try {
|
|
20
|
+
return e.fileMatches(file);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
has(framework) {
|
|
28
|
+
return this.byName.has(framework);
|
|
29
|
+
}
|
|
30
|
+
get(framework) {
|
|
31
|
+
return this.byName.get(framework);
|
|
32
|
+
}
|
|
33
|
+
size() {
|
|
34
|
+
return this.byName.size;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { IEdge, INode } from '@shrkcrft/graph';
|
|
2
|
+
import type { FrameworkName } from '../schema/framework-schema.js';
|
|
3
|
+
/**
|
|
4
|
+
* Plugin contract for a framework extractor.
|
|
5
|
+
*
|
|
6
|
+
* Each extractor inspects a single source file and returns the
|
|
7
|
+
* framework-specific nodes + edges it produces. The runner walks the
|
|
8
|
+
* project, applies every extractor whose `fileMatches` returns true,
|
|
9
|
+
* dedupes, and writes the result to the framework store.
|
|
10
|
+
*
|
|
11
|
+
* Extractors must be **side-effect free** and deterministic. They take
|
|
12
|
+
* file path + content and return data — no fs writes, no caching of
|
|
13
|
+
* their own.
|
|
14
|
+
*/
|
|
15
|
+
export interface IFrameworkExtractor {
|
|
16
|
+
/** Stable name (e.g. 'nestjs', 'react'). */
|
|
17
|
+
framework: FrameworkName;
|
|
18
|
+
/** Display label for diagnostics. */
|
|
19
|
+
label: string;
|
|
20
|
+
/**
|
|
21
|
+
* Fast pre-filter: should the AST extractor pass run for this file?
|
|
22
|
+
* Typically checks the path (e.g. `*.tsx`) and/or content fingerprint
|
|
23
|
+
* (e.g. presence of `'@nestjs/common'` import or JSX).
|
|
24
|
+
*/
|
|
25
|
+
fileMatches(file: {
|
|
26
|
+
path: string;
|
|
27
|
+
content: string;
|
|
28
|
+
}): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Run the full extraction against the file. Called only when
|
|
31
|
+
* `fileMatches` returns true. Errors raised here are converted to
|
|
32
|
+
* diagnostics by the runner — never propagated out of an index build.
|
|
33
|
+
*/
|
|
34
|
+
extract(input: IExtractInput): IExtractOutput;
|
|
35
|
+
}
|
|
36
|
+
export interface IExtractInput {
|
|
37
|
+
/** Project-relative POSIX path. */
|
|
38
|
+
filePath: string;
|
|
39
|
+
/** File contents. */
|
|
40
|
+
content: string;
|
|
41
|
+
/** Code-graph node id for the file (`file:<path>`). */
|
|
42
|
+
fileNodeId: string;
|
|
43
|
+
}
|
|
44
|
+
export interface IExtractOutput {
|
|
45
|
+
nodes: readonly INode[];
|
|
46
|
+
edges: readonly IEdge[];
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=framework-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framework-extractor.d.ts","sourceRoot":"","sources":["../../src/extractor-api/framework-extractor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAEnE;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,mBAAmB;IAClC,4CAA4C;IAC5C,SAAS,EAAE,aAAa,CAAC;IACzB,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,WAAW,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;IAC9D;;;;OAIG;IACH,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,cAAc,CAAC;CAC/C;AAED,MAAM,WAAW,aAAa;IAC5B,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,SAAS,KAAK,EAAE,CAAC;IACxB,KAAK,EAAE,SAAS,KAAK,EAAE,CAAC;CACzB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IFrameworkExtractor } from '../extractor-api/framework-extractor.js';
|
|
2
|
+
export declare const ANGULAR_EXTRACTOR_SOURCE = "angular-extractor@v1";
|
|
3
|
+
/**
|
|
4
|
+
* Angular extractor.
|
|
5
|
+
*
|
|
6
|
+
* Detects classes carrying any of the @Component / @Directive / @Pipe /
|
|
7
|
+
* @Injectable / @NgModule decorators. Emits one `FrameworkEntity` per
|
|
8
|
+
* decorated class, with the subtype derived from the decorator name.
|
|
9
|
+
*
|
|
10
|
+
* Component-specific metadata (selector, standalone, templateUrl) is
|
|
11
|
+
* lifted into `data` so the agent / dashboard can render it without a
|
|
12
|
+
* second AST walk.
|
|
13
|
+
*
|
|
14
|
+
* Out of scope (Wave 7 follow-up): template parsing, DI graph between
|
|
15
|
+
* services, route configurations. Each is its own pass.
|
|
16
|
+
*/
|
|
17
|
+
export declare const angularExtractor: IFrameworkExtractor;
|
|
18
|
+
//# sourceMappingURL=angular-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"angular-extractor.d.ts","sourceRoot":"","sources":["../../src/extractors/angular-extractor.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,yCAAyC,CAAC;AAEjD,eAAO,MAAM,wBAAwB,yBAAyB,CAAC;AAoB/D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,EAAE,mBAqC9B,CAAC"}
|
|
@@ -0,0 +1,175 @@
|
|
|
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 ANGULAR_EXTRACTOR_SOURCE = 'angular-extractor@v1';
|
|
6
|
+
const FAST_FILTER_NEEDLES = [
|
|
7
|
+
"from '@angular/core'",
|
|
8
|
+
'from "@angular/core"',
|
|
9
|
+
'@Component',
|
|
10
|
+
'@Directive',
|
|
11
|
+
'@NgModule',
|
|
12
|
+
'@Injectable',
|
|
13
|
+
'@Pipe',
|
|
14
|
+
];
|
|
15
|
+
const DECORATOR_TO_SUBTYPE = {
|
|
16
|
+
Component: 'component',
|
|
17
|
+
Directive: 'directive',
|
|
18
|
+
Pipe: 'pipe',
|
|
19
|
+
Injectable: 'service',
|
|
20
|
+
NgModule: 'module',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Angular extractor.
|
|
24
|
+
*
|
|
25
|
+
* Detects classes carrying any of the @Component / @Directive / @Pipe /
|
|
26
|
+
* @Injectable / @NgModule decorators. Emits one `FrameworkEntity` per
|
|
27
|
+
* decorated class, with the subtype derived from the decorator name.
|
|
28
|
+
*
|
|
29
|
+
* Component-specific metadata (selector, standalone, templateUrl) is
|
|
30
|
+
* lifted into `data` so the agent / dashboard can render it without a
|
|
31
|
+
* second AST walk.
|
|
32
|
+
*
|
|
33
|
+
* Out of scope (Wave 7 follow-up): template parsing, DI graph between
|
|
34
|
+
* services, route configurations. Each is its own pass.
|
|
35
|
+
*/
|
|
36
|
+
export const angularExtractor = {
|
|
37
|
+
framework: 'angular',
|
|
38
|
+
label: 'Angular',
|
|
39
|
+
fileMatches({ path, content }) {
|
|
40
|
+
if (!/\.(?:t|j)sx?$/.test(path))
|
|
41
|
+
return false;
|
|
42
|
+
for (const needle of FAST_FILTER_NEEDLES) {
|
|
43
|
+
if (content.includes(needle))
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
},
|
|
48
|
+
extract(input) {
|
|
49
|
+
const nodes = [];
|
|
50
|
+
const edges = [];
|
|
51
|
+
const sf = parse(input);
|
|
52
|
+
if (!sf)
|
|
53
|
+
return { nodes, edges };
|
|
54
|
+
const visit = (node) => {
|
|
55
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
56
|
+
const decorators = collectDecorators(node);
|
|
57
|
+
for (const d of decorators) {
|
|
58
|
+
const subtype = DECORATOR_TO_SUBTYPE[d.name];
|
|
59
|
+
if (!subtype)
|
|
60
|
+
continue;
|
|
61
|
+
const extra = extractDecoratorMetadata(d, subtype);
|
|
62
|
+
const entity = makeEntity(input, node.name.text, subtype, extra);
|
|
63
|
+
nodes.push(entity);
|
|
64
|
+
edges.push(edge(input.fileNodeId, entity.id, EdgeKind.FrameworkDeclares, { subtype }));
|
|
65
|
+
// A class can only carry one Angular role decorator in
|
|
66
|
+
// practice; if a future class somehow has two we'd over-count
|
|
67
|
+
// but the IDs are deterministic so duplicates collapse.
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
ts.forEachChild(node, visit);
|
|
72
|
+
};
|
|
73
|
+
ts.forEachChild(sf, visit);
|
|
74
|
+
return { nodes, edges };
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
function collectDecorators(node) {
|
|
78
|
+
if (!ts.canHaveDecorators(node))
|
|
79
|
+
return [];
|
|
80
|
+
const decorators = ts.getDecorators(node) ?? [];
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const d of decorators) {
|
|
83
|
+
const expr = d.expression;
|
|
84
|
+
if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) {
|
|
85
|
+
out.push({ name: expr.expression.text, callArguments: expr.arguments });
|
|
86
|
+
}
|
|
87
|
+
else if (ts.isIdentifier(expr)) {
|
|
88
|
+
out.push({ name: expr.text });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function extractDecoratorMetadata(d, subtype) {
|
|
94
|
+
if (!d.callArguments || d.callArguments.length === 0)
|
|
95
|
+
return {};
|
|
96
|
+
const arg0 = d.callArguments[0];
|
|
97
|
+
if (!arg0 || !ts.isObjectLiteralExpression(arg0))
|
|
98
|
+
return {};
|
|
99
|
+
const out = {};
|
|
100
|
+
for (const prop of arg0.properties) {
|
|
101
|
+
if (!ts.isPropertyAssignment(prop))
|
|
102
|
+
continue;
|
|
103
|
+
if (!ts.isIdentifier(prop.name))
|
|
104
|
+
continue;
|
|
105
|
+
const key = prop.name.text;
|
|
106
|
+
const value = prop.initializer;
|
|
107
|
+
// Lift the most useful string fields. Arrays of identifiers (e.g.
|
|
108
|
+
// `imports: [CommonModule, MyOtherModule]`) become arrays of names.
|
|
109
|
+
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) {
|
|
110
|
+
if (subtype === 'component' && (key === 'selector' || key === 'templateUrl' || key === 'styleUrl')) {
|
|
111
|
+
out[key] = value.text;
|
|
112
|
+
}
|
|
113
|
+
else if (subtype === 'directive' && key === 'selector') {
|
|
114
|
+
out[key] = value.text;
|
|
115
|
+
}
|
|
116
|
+
else if (subtype === 'pipe' && key === 'name') {
|
|
117
|
+
out[key] = value.text;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword) {
|
|
121
|
+
if (key === 'standalone')
|
|
122
|
+
out['standalone'] = value.kind === ts.SyntaxKind.TrueKeyword;
|
|
123
|
+
}
|
|
124
|
+
else if (ts.isArrayLiteralExpression(value)) {
|
|
125
|
+
if (subtype === 'module' && (key === 'imports' || key === 'declarations' || key === 'providers' || key === 'exports')) {
|
|
126
|
+
out[key] = arrayOfIdentifierNames(value);
|
|
127
|
+
}
|
|
128
|
+
else if (subtype === 'component' && key === 'imports') {
|
|
129
|
+
out[key] = arrayOfIdentifierNames(value);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
function arrayOfIdentifierNames(arr) {
|
|
136
|
+
const out = [];
|
|
137
|
+
for (const el of arr.elements) {
|
|
138
|
+
if (ts.isIdentifier(el))
|
|
139
|
+
out.push(el.text);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
function makeEntity(input, className, subtype, extra) {
|
|
144
|
+
return {
|
|
145
|
+
id: `framework:angular:${subtype}:${input.filePath}#${className}`,
|
|
146
|
+
kind: NodeKind.FrameworkEntity,
|
|
147
|
+
label: className,
|
|
148
|
+
path: input.filePath,
|
|
149
|
+
tags: ['angular', subtype],
|
|
150
|
+
data: { framework: 'angular', subtype, className, ...extra },
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function edge(from, to, kind, data) {
|
|
154
|
+
return {
|
|
155
|
+
id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
|
|
156
|
+
from,
|
|
157
|
+
to,
|
|
158
|
+
kind,
|
|
159
|
+
source: ANGULAR_EXTRACTOR_SOURCE,
|
|
160
|
+
...(data ? { data } : {}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function parse(input) {
|
|
164
|
+
const ext = nodePath.extname(input.filePath).toLowerCase();
|
|
165
|
+
const kind = ext === '.tsx' ? ts.ScriptKind.TSX
|
|
166
|
+
: ext === '.jsx' ? ts.ScriptKind.JSX
|
|
167
|
+
: ext === '.js' || ext === '.mjs' || ext === '.cjs' ? ts.ScriptKind.JS
|
|
168
|
+
: ts.ScriptKind.TS;
|
|
169
|
+
try {
|
|
170
|
+
return ts.createSourceFile(input.filePath, input.content, ts.ScriptTarget.Latest, true, kind);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IFrameworkExtractor } from '../extractor-api/framework-extractor.js';
|
|
2
|
+
export declare const ASTRO_EXTRACTOR_SOURCE = "astro-extractor@v1";
|
|
3
|
+
/**
|
|
4
|
+
* Astro extractor.
|
|
5
|
+
*
|
|
6
|
+
* Astro uses file-based routing under `src/pages/`. Detection is
|
|
7
|
+
* entirely path-based; the content is not parsed (Astro has its own
|
|
8
|
+
* frontmatter / template DSL that's out of scope for the MVP).
|
|
9
|
+
*
|
|
10
|
+
* - `src/pages/**\/*.astro` → page (with derived route path)
|
|
11
|
+
* - `**\/*.astro` outside pages/ → component
|
|
12
|
+
* - `src/pages/**\/*.{ts,js}` → API route (server-side endpoint)
|
|
13
|
+
*/
|
|
14
|
+
export declare const astroExtractor: IFrameworkExtractor;
|
|
15
|
+
//# sourceMappingURL=astro-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"astro-extractor.d.ts","sourceRoot":"","sources":["../../src/extractors/astro-extractor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,yCAAyC,CAAC;AAEjD,eAAO,MAAM,sBAAsB,uBAAuB,CAAC;AAM3D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,EAAE,mBAsD5B,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { EdgeKind, NodeKind } from '@shrkcrft/graph';
|
|
3
|
+
export const ASTRO_EXTRACTOR_SOURCE = 'astro-extractor@v1';
|
|
4
|
+
const PAGE_RE = /(?:^|\/)src\/pages\/(.+?)\.astro$/;
|
|
5
|
+
const COMPONENT_RE = /\.astro$/;
|
|
6
|
+
const API_ROUTE_RE = /(?:^|\/)src\/pages\/(.+?)\.(?:ts|js)$/;
|
|
7
|
+
/**
|
|
8
|
+
* Astro extractor.
|
|
9
|
+
*
|
|
10
|
+
* Astro uses file-based routing under `src/pages/`. Detection is
|
|
11
|
+
* entirely path-based; the content is not parsed (Astro has its own
|
|
12
|
+
* frontmatter / template DSL that's out of scope for the MVP).
|
|
13
|
+
*
|
|
14
|
+
* - `src/pages/**\/*.astro` → page (with derived route path)
|
|
15
|
+
* - `**\/*.astro` outside pages/ → component
|
|
16
|
+
* - `src/pages/**\/*.{ts,js}` → API route (server-side endpoint)
|
|
17
|
+
*/
|
|
18
|
+
export const astroExtractor = {
|
|
19
|
+
framework: 'astro',
|
|
20
|
+
label: 'Astro',
|
|
21
|
+
fileMatches({ path }) {
|
|
22
|
+
return path.endsWith('.astro') || API_ROUTE_RE.test(path);
|
|
23
|
+
},
|
|
24
|
+
extract(input) {
|
|
25
|
+
const nodes = [];
|
|
26
|
+
const edges = [];
|
|
27
|
+
const pageMatch = PAGE_RE.exec(input.filePath);
|
|
28
|
+
if (pageMatch) {
|
|
29
|
+
const route = astroRoute(pageMatch[1]);
|
|
30
|
+
const e = makeEntity(input, 'page', `page ${route}`, { kind: 'page', routePath: route });
|
|
31
|
+
nodes.push(e);
|
|
32
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'page' }));
|
|
33
|
+
return { nodes, edges };
|
|
34
|
+
}
|
|
35
|
+
const apiMatch = API_ROUTE_RE.exec(input.filePath);
|
|
36
|
+
if (apiMatch) {
|
|
37
|
+
const route = '/' + normalize(apiMatch[1]);
|
|
38
|
+
// Astro API routes export one binding per HTTP verb (`export const
|
|
39
|
+
// GET = ...`, `export async function POST(...) { ... }`, etc.).
|
|
40
|
+
// Detect each exported method and emit a per-method route entity.
|
|
41
|
+
// If no method exports are found, fall back to a single
|
|
42
|
+
// unspecified `api-route` entity so the file still surfaces.
|
|
43
|
+
const methods = detectAstroApiMethods(input.content);
|
|
44
|
+
if (methods.length === 0) {
|
|
45
|
+
const e = makeEntity(input, 'api-route', `API ${route}`, { kind: 'api-route', routePath: route });
|
|
46
|
+
nodes.push(e);
|
|
47
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'api-route' }));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
for (const method of methods) {
|
|
51
|
+
const e = makeEntity(input, 'api-route', `${method} ${route}`, {
|
|
52
|
+
kind: 'api-route',
|
|
53
|
+
routePath: route,
|
|
54
|
+
method,
|
|
55
|
+
});
|
|
56
|
+
nodes.push(e);
|
|
57
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'api-route', method }));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { nodes, edges };
|
|
61
|
+
}
|
|
62
|
+
if (COMPONENT_RE.test(input.filePath)) {
|
|
63
|
+
const filename = input.filePath.split('/').pop().replace(/\.astro$/, '');
|
|
64
|
+
const e = makeEntity(input, 'component', filename, { kind: 'component', name: filename });
|
|
65
|
+
nodes.push(e);
|
|
66
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'component' }));
|
|
67
|
+
}
|
|
68
|
+
return { nodes, edges };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
function astroRoute(p) {
|
|
72
|
+
return '/' + normalize(p);
|
|
73
|
+
}
|
|
74
|
+
function normalize(p) {
|
|
75
|
+
const parts = p.split('/').filter((s) => s.length > 0);
|
|
76
|
+
const out = [];
|
|
77
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
78
|
+
const seg = parts[i];
|
|
79
|
+
if (i === parts.length - 1 && seg === 'index')
|
|
80
|
+
continue;
|
|
81
|
+
// Astro dynamic segments: `[id].astro` → `:id`, `[...slug].astro` → `*`.
|
|
82
|
+
if (/^\[\.\.\..+\]$/.test(seg))
|
|
83
|
+
out.push('*');
|
|
84
|
+
else if (/^\[.+\]$/.test(seg))
|
|
85
|
+
out.push(':' + seg.slice(1, -1));
|
|
86
|
+
else
|
|
87
|
+
out.push(seg);
|
|
88
|
+
}
|
|
89
|
+
return out.join('/');
|
|
90
|
+
}
|
|
91
|
+
function makeEntity(input, subtype, label, data) {
|
|
92
|
+
return {
|
|
93
|
+
id: `framework:astro:${subtype}:${input.filePath}#${label}`,
|
|
94
|
+
kind: NodeKind.FrameworkEntity,
|
|
95
|
+
label,
|
|
96
|
+
path: input.filePath,
|
|
97
|
+
tags: ['astro', subtype],
|
|
98
|
+
data: { framework: 'astro', subtype, ...data },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function edge(from, to, kind, data) {
|
|
102
|
+
return {
|
|
103
|
+
id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
|
|
104
|
+
from,
|
|
105
|
+
to,
|
|
106
|
+
kind,
|
|
107
|
+
source: ASTRO_EXTRACTOR_SOURCE,
|
|
108
|
+
...(data ? { data } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const ASTRO_API_METHOD_NAMES = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'ALL'];
|
|
112
|
+
/**
|
|
113
|
+
* Detect which HTTP verbs an Astro API endpoint file exports. Matches:
|
|
114
|
+
* - `export const GET = ...`
|
|
115
|
+
* - `export let POST = ...`
|
|
116
|
+
* - `export async function PUT(...) { ... }`
|
|
117
|
+
* - `export function DELETE(...) { ... }`
|
|
118
|
+
* Returns the unique set of detected verb names, in canonical order.
|
|
119
|
+
*/
|
|
120
|
+
function detectAstroApiMethods(content) {
|
|
121
|
+
const found = new Set();
|
|
122
|
+
for (const m of ASTRO_API_METHOD_NAMES) {
|
|
123
|
+
const re = new RegExp(`^\\s*export\\s+(?:const|let|var|(?:async\\s+)?function)\\s+${m}\\b`, 'm');
|
|
124
|
+
if (re.test(content))
|
|
125
|
+
found.add(m);
|
|
126
|
+
}
|
|
127
|
+
return ASTRO_API_METHOD_NAMES.filter((m) => found.has(m));
|
|
128
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IFrameworkExtractor } from '../extractor-api/framework-extractor.js';
|
|
2
|
+
export declare const DJANGO_EXTRACTOR_SOURCE = "django-extractor@v1";
|
|
3
|
+
/**
|
|
4
|
+
* Django extractor.
|
|
5
|
+
*
|
|
6
|
+
* Regex-only (Python is not parsed via the TS AST). Detection:
|
|
7
|
+
* - Class inheriting from `models.Model`, `models.AbstractUser`, or
|
|
8
|
+
* another known Django base → **model** entity.
|
|
9
|
+
* - Class inheriting from `View`, `TemplateView`, `ListView`, etc. →
|
|
10
|
+
* **view** entity.
|
|
11
|
+
* - Function with a leading parameter named `request` (heuristic for
|
|
12
|
+
* function-based views) → **view** entity.
|
|
13
|
+
* - `path('<route>', view, name='...')` / `re_path(...)` inside a
|
|
14
|
+
* `urls.py`-like file → **url-pattern** entity. Detection is loose;
|
|
15
|
+
* captures the route string + view name when expressible.
|
|
16
|
+
*
|
|
17
|
+
* Out of scope:
|
|
18
|
+
* - Admin registrations.
|
|
19
|
+
* - Middleware classes.
|
|
20
|
+
* - Migration files (Django generates these; treating them as code
|
|
21
|
+
* would be noisy).
|
|
22
|
+
*/
|
|
23
|
+
export declare const djangoExtractor: IFrameworkExtractor;
|
|
24
|
+
//# sourceMappingURL=django-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"django-extractor.d.ts","sourceRoot":"","sources":["../../src/extractors/django-extractor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,yCAAyC,CAAC;AAEjD,eAAO,MAAM,uBAAuB,wBAAwB,CAAC;AAc7D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,mBAqE7B,CAAC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { EdgeKind, NodeKind } from '@shrkcrft/graph';
|
|
3
|
+
export const DJANGO_EXTRACTOR_SOURCE = 'django-extractor@v1';
|
|
4
|
+
const FAST_FILTER_NEEDLES = [
|
|
5
|
+
'from django',
|
|
6
|
+
'django.db',
|
|
7
|
+
'django.urls',
|
|
8
|
+
'django.views',
|
|
9
|
+
'django.http',
|
|
10
|
+
'models.Model',
|
|
11
|
+
'TemplateView',
|
|
12
|
+
'path(',
|
|
13
|
+
're_path(',
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Django extractor.
|
|
17
|
+
*
|
|
18
|
+
* Regex-only (Python is not parsed via the TS AST). Detection:
|
|
19
|
+
* - Class inheriting from `models.Model`, `models.AbstractUser`, or
|
|
20
|
+
* another known Django base → **model** entity.
|
|
21
|
+
* - Class inheriting from `View`, `TemplateView`, `ListView`, etc. →
|
|
22
|
+
* **view** entity.
|
|
23
|
+
* - Function with a leading parameter named `request` (heuristic for
|
|
24
|
+
* function-based views) → **view** entity.
|
|
25
|
+
* - `path('<route>', view, name='...')` / `re_path(...)` inside a
|
|
26
|
+
* `urls.py`-like file → **url-pattern** entity. Detection is loose;
|
|
27
|
+
* captures the route string + view name when expressible.
|
|
28
|
+
*
|
|
29
|
+
* Out of scope:
|
|
30
|
+
* - Admin registrations.
|
|
31
|
+
* - Middleware classes.
|
|
32
|
+
* - Migration files (Django generates these; treating them as code
|
|
33
|
+
* would be noisy).
|
|
34
|
+
*/
|
|
35
|
+
export const djangoExtractor = {
|
|
36
|
+
framework: 'django',
|
|
37
|
+
label: 'Django',
|
|
38
|
+
fileMatches({ path, content }) {
|
|
39
|
+
if (!path.endsWith('.py'))
|
|
40
|
+
return false;
|
|
41
|
+
for (const needle of FAST_FILTER_NEEDLES) {
|
|
42
|
+
if (content.includes(needle))
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
},
|
|
47
|
+
extract(input) {
|
|
48
|
+
const nodes = [];
|
|
49
|
+
const edges = [];
|
|
50
|
+
const isUrlsFile = /(?:^|\/)urls\.py$/.test(input.filePath);
|
|
51
|
+
const lines = input.content.split('\n');
|
|
52
|
+
// Models: `class Foo(models.Model):` or `class Foo(AbstractBaseUser):`.
|
|
53
|
+
const modelRe = /^class\s+([A-Za-z_]\w*)\s*\((?:[\w.]*\.)?(?:Model|AbstractUser|AbstractBaseUser|AbstractModel|TimeStampedModel)\b/;
|
|
54
|
+
// CBVs: a class whose first base name ends in `View`.
|
|
55
|
+
const viewRe = /^class\s+([A-Za-z_]\w*)\s*\(([^)]+)\)/;
|
|
56
|
+
// FBVs: function whose first parameter is `request`.
|
|
57
|
+
const fbvRe = /^(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(\s*request\b/;
|
|
58
|
+
// URL pattern: `path('<route>', view, ...)` or `re_path(...)`.
|
|
59
|
+
const urlRe = /(?:^|\s)(?:path|re_path|url)\s*\(\s*['"]([^'"]*)['"]\s*,\s*([A-Za-z_][\w.]*)(?:\.as_view\(\))?/;
|
|
60
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
61
|
+
const raw = lines[i];
|
|
62
|
+
const trimmed = raw.trim();
|
|
63
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
64
|
+
continue;
|
|
65
|
+
const line = i + 1;
|
|
66
|
+
let m;
|
|
67
|
+
if ((m = modelRe.exec(raw))) {
|
|
68
|
+
const e = makeEntity(input, 'model', m[1], { className: m[1] });
|
|
69
|
+
nodes.push(e);
|
|
70
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'model', line }));
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if ((m = viewRe.exec(raw))) {
|
|
74
|
+
const className = m[1];
|
|
75
|
+
const bases = m[2].split(',').map((s) => s.trim());
|
|
76
|
+
// Pick first base that looks like a view (ends in 'View') and isn't a model base.
|
|
77
|
+
if (bases.some((b) => /View$/.test(b)) && !bases.some((b) => /Model$|AbstractUser|AbstractBaseUser/.test(b))) {
|
|
78
|
+
const e = makeEntity(input, 'view', className, { className, kind: 'cbv', bases });
|
|
79
|
+
nodes.push(e);
|
|
80
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'view', line }));
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if ((m = fbvRe.exec(raw))) {
|
|
85
|
+
const name = m[1];
|
|
86
|
+
const e = makeEntity(input, 'view', name, { kind: 'fbv' });
|
|
87
|
+
nodes.push(e);
|
|
88
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'view', line }));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (isUrlsFile && (m = urlRe.exec(raw))) {
|
|
92
|
+
const route = m[1];
|
|
93
|
+
const target = m[2];
|
|
94
|
+
const e = makeEntity(input, 'url-pattern', `${route} → ${target}`, {
|
|
95
|
+
route,
|
|
96
|
+
target,
|
|
97
|
+
});
|
|
98
|
+
nodes.push(e);
|
|
99
|
+
edges.push(edge(input.fileNodeId, e.id, EdgeKind.FrameworkDeclares, { subtype: 'url-pattern', line }));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { nodes, edges };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
function makeEntity(input, subtype, label, extra) {
|
|
106
|
+
return {
|
|
107
|
+
id: `framework:django:${subtype}:${input.filePath}#${label}`,
|
|
108
|
+
kind: NodeKind.FrameworkEntity,
|
|
109
|
+
label,
|
|
110
|
+
path: input.filePath,
|
|
111
|
+
tags: ['django', subtype],
|
|
112
|
+
data: { framework: 'django', subtype, ...extra },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function edge(from, to, kind, data) {
|
|
116
|
+
return {
|
|
117
|
+
id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
|
|
118
|
+
from,
|
|
119
|
+
to,
|
|
120
|
+
kind,
|
|
121
|
+
source: DJANGO_EXTRACTOR_SOURCE,
|
|
122
|
+
...(data ? { data } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IFrameworkExtractor } from '../extractor-api/framework-extractor.js';
|
|
2
|
+
export declare const EXPRESS_EXTRACTOR_SOURCE = "express-extractor@v1";
|
|
3
|
+
/**
|
|
4
|
+
* Express extractor.
|
|
5
|
+
*
|
|
6
|
+
* Express has no decorators, so detection is signature-based:
|
|
7
|
+
* - A variable initialized with a call to `express()` or
|
|
8
|
+
* `express.Router()` (or to `Router(...)` when `Router` was
|
|
9
|
+
* imported from `'express'`) becomes a **router** entity.
|
|
10
|
+
* - Subsequent `<router>.get(path, …)` etc. calls become **route**
|
|
11
|
+
* entities with method + path data.
|
|
12
|
+
*
|
|
13
|
+
* Middleware chains (extra handlers in the call args) are recorded on
|
|
14
|
+
* the route entity's `data.middlewareCount`. Detailed middleware-node
|
|
15
|
+
* extraction is out of scope for the MVP.
|
|
16
|
+
*/
|
|
17
|
+
export declare const expressExtractor: IFrameworkExtractor;
|
|
18
|
+
//# sourceMappingURL=express-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express-extractor.d.ts","sourceRoot":"","sources":["../../src/extractors/express-extractor.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,yCAAyC,CAAC;AAEjD,eAAO,MAAM,wBAAwB,yBAAyB,CAAC;AAgB/D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,EAAE,mBAwE9B,CAAC"}
|