@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,125 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { EdgeKind, NodeKind } from '@shrkcrft/graph';
|
|
3
|
+
export const VUE_EXTRACTOR_SOURCE = 'vue-extractor@v1';
|
|
4
|
+
const SCRIPT_OPEN_RE = /<script\b[^>]*>/i;
|
|
5
|
+
const SCRIPT_CLOSE_RE = /<\/script>/i;
|
|
6
|
+
const SETUP_RE = /<script[^>]*\bsetup\b[^>]*>/i;
|
|
7
|
+
const DEFINE_COMPONENT_RE = /\b(defineComponent|defineAsyncComponent)\s*\(/;
|
|
8
|
+
const COMPOSITION_HOOK_RE = /\b(ref|reactive|computed|watch|onMounted|onUnmounted|onBeforeMount|onUpdated)\s*\(/g;
|
|
9
|
+
const NAME_OPTION_RE = /\bname\s*:\s*['"`]([^'"`]+)['"`]/;
|
|
10
|
+
/**
|
|
11
|
+
* Vue extractor.
|
|
12
|
+
*
|
|
13
|
+
* Detection sources (in order):
|
|
14
|
+
* 1. `.vue` SFC files — always a component, named after the filename.
|
|
15
|
+
* 2. `.ts` / `.tsx` / `.js` / `.jsx` files containing
|
|
16
|
+
* `defineComponent(` or `defineAsyncComponent(` — emits a
|
|
17
|
+
* component entity per call.
|
|
18
|
+
*
|
|
19
|
+
* For SFC files we also detect Composition API hook usages from the
|
|
20
|
+
* `<script>` block via regex (lightweight — no template parsing).
|
|
21
|
+
*
|
|
22
|
+
* Out of scope: template parsing, <style> block detection, scoped slots.
|
|
23
|
+
*/
|
|
24
|
+
export const vueExtractor = {
|
|
25
|
+
framework: 'vue',
|
|
26
|
+
label: 'Vue',
|
|
27
|
+
fileMatches({ path, content }) {
|
|
28
|
+
if (path.endsWith('.vue'))
|
|
29
|
+
return true;
|
|
30
|
+
if (!/\.(?:t|j)sx?$/.test(path))
|
|
31
|
+
return false;
|
|
32
|
+
return DEFINE_COMPONENT_RE.test(content) || content.includes("from 'vue'") || content.includes('from "vue"');
|
|
33
|
+
},
|
|
34
|
+
extract(input) {
|
|
35
|
+
const nodes = [];
|
|
36
|
+
const edges = [];
|
|
37
|
+
if (input.filePath.endsWith('.vue')) {
|
|
38
|
+
extractFromSfc(input, nodes, edges);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
extractFromTs(input, nodes, edges);
|
|
42
|
+
}
|
|
43
|
+
return { nodes, edges };
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
function extractFromSfc(input, nodes, edges) {
|
|
47
|
+
const script = extractScriptBlock(input.content);
|
|
48
|
+
const isSetup = SETUP_RE.test(input.content);
|
|
49
|
+
// Component name from filename (without ext); honor `name:` option if present in script.
|
|
50
|
+
const filename = input.filePath.split('/').pop().replace(/\.vue$/, '');
|
|
51
|
+
let name = filename;
|
|
52
|
+
if (!isSetup) {
|
|
53
|
+
const m = script.match(NAME_OPTION_RE);
|
|
54
|
+
if (m)
|
|
55
|
+
name = m[1];
|
|
56
|
+
}
|
|
57
|
+
const entity = makeEntity(input, 'component', name, {
|
|
58
|
+
sfc: true,
|
|
59
|
+
setup: isSetup,
|
|
60
|
+
});
|
|
61
|
+
nodes.push(entity);
|
|
62
|
+
edges.push(edge(input.fileNodeId, entity.id, EdgeKind.FrameworkDeclares, { subtype: 'component' }));
|
|
63
|
+
// Composition API hook usages.
|
|
64
|
+
const hooks = new Set();
|
|
65
|
+
let m;
|
|
66
|
+
COMPOSITION_HOOK_RE.lastIndex = 0;
|
|
67
|
+
while ((m = COMPOSITION_HOOK_RE.exec(script)) !== null) {
|
|
68
|
+
hooks.add(m[1]);
|
|
69
|
+
}
|
|
70
|
+
for (const hook of hooks) {
|
|
71
|
+
const hookEntity = makeEntity(input, 'hook-usage', hook, { hook });
|
|
72
|
+
nodes.push(hookEntity);
|
|
73
|
+
edges.push(edge(entity.id, hookEntity.id, EdgeKind.UsesHook, { hook }));
|
|
74
|
+
edges.push(edge(input.fileNodeId, hookEntity.id, EdgeKind.FrameworkDeclares, { subtype: 'hook-usage' }));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function extractFromTs(input, nodes, edges) {
|
|
78
|
+
// Walk every `defineComponent(...)` call. We don't need an AST —
|
|
79
|
+
// regex with a captured-string `name:` option is enough for the MVP.
|
|
80
|
+
const re = /\bdefineComponent\s*\(\s*\{([^}]*)\}/g;
|
|
81
|
+
let m;
|
|
82
|
+
let idx = 0;
|
|
83
|
+
while ((m = re.exec(input.content)) !== null) {
|
|
84
|
+
idx += 1;
|
|
85
|
+
const body = m[1] ?? '';
|
|
86
|
+
const nameMatch = body.match(NAME_OPTION_RE);
|
|
87
|
+
const name = nameMatch ? nameMatch[1] : `Component${idx}`;
|
|
88
|
+
const entity = makeEntity(input, 'component', name, { sfc: false });
|
|
89
|
+
nodes.push(entity);
|
|
90
|
+
edges.push(edge(input.fileNodeId, entity.id, EdgeKind.FrameworkDeclares, { subtype: 'component' }));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function extractScriptBlock(content) {
|
|
94
|
+
const open = content.search(SCRIPT_OPEN_RE);
|
|
95
|
+
if (open < 0)
|
|
96
|
+
return '';
|
|
97
|
+
const after = content.slice(open).indexOf('>');
|
|
98
|
+
if (after < 0)
|
|
99
|
+
return '';
|
|
100
|
+
const start = open + after + 1;
|
|
101
|
+
const closeIdx = content.slice(start).search(SCRIPT_CLOSE_RE);
|
|
102
|
+
if (closeIdx < 0)
|
|
103
|
+
return content.slice(start);
|
|
104
|
+
return content.slice(start, start + closeIdx);
|
|
105
|
+
}
|
|
106
|
+
function makeEntity(input, subtype, name, extra) {
|
|
107
|
+
return {
|
|
108
|
+
id: `framework:vue:${subtype}:${input.filePath}#${name}`,
|
|
109
|
+
kind: NodeKind.FrameworkEntity,
|
|
110
|
+
label: name,
|
|
111
|
+
path: input.filePath,
|
|
112
|
+
tags: ['vue', subtype],
|
|
113
|
+
data: { framework: 'vue', subtype, name, ...extra },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function edge(from, to, kind, data) {
|
|
117
|
+
return {
|
|
118
|
+
id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
|
|
119
|
+
from,
|
|
120
|
+
to,
|
|
121
|
+
kind,
|
|
122
|
+
source: VUE_EXTRACTOR_SOURCE,
|
|
123
|
+
...(data ? { data } : {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export * from './schema/framework-schema.js';
|
|
2
|
+
export * from './extractor-api/framework-extractor.js';
|
|
3
|
+
export * from './extractor-api/extractor-registry.js';
|
|
4
|
+
export * from './extractors/nestjs-extractor.js';
|
|
5
|
+
export * from './extractors/react-extractor.js';
|
|
6
|
+
export * from './extractors/express-extractor.js';
|
|
7
|
+
export * from './extractors/nextjs-extractor.js';
|
|
8
|
+
export * from './extractors/angular-extractor.js';
|
|
9
|
+
export * from './extractors/vue-extractor.js';
|
|
10
|
+
export * from './extractors/svelte-extractor.js';
|
|
11
|
+
export * from './extractors/fastify-extractor.js';
|
|
12
|
+
export * from './extractors/fastapi-extractor.js';
|
|
13
|
+
export * from './extractors/solid-extractor.js';
|
|
14
|
+
export * from './extractors/astro-extractor.js';
|
|
15
|
+
export * from './extractors/django-extractor.js';
|
|
16
|
+
export * from './extractors/flask-extractor.js';
|
|
17
|
+
export * from './extractors/spring-extractor.js';
|
|
18
|
+
export * from './extractors/rails-extractor.js';
|
|
19
|
+
export * from './extractors/phoenix-extractor.js';
|
|
20
|
+
export * from './extractors/graphql-extractor.js';
|
|
21
|
+
export * from './extractors/laravel-extractor.js';
|
|
22
|
+
export * from './extractors/flutter-extractor.js';
|
|
23
|
+
export * from './store/framework-store.js';
|
|
24
|
+
export * from './runner/run-extractors.js';
|
|
25
|
+
export * from './runner/load-pack-extractors.js';
|
|
26
|
+
export * from './query/framework-query-api.js';
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wCAAwC,CAAC;AACvD,cAAc,uCAAuC,CAAC;AACtD,cAAc,kCAAkC,CAAC;AACjD,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC;AAClD,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,mCAAmC,CAAC;AAClD,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,kCAAkC,CAAC;AACjD,cAAc,iCAAiC,CAAC;AAChD,cAAc,kCAAkC,CAAC;AACjD,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC;AAClD,cAAc,mCAAmC,CAAC;AAClD,cAAc,mCAAmC,CAAC;AAClD,cAAc,mCAAmC,CAAC;AAClD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,kCAAkC,CAAC;AACjD,cAAc,gCAAgC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export * from "./schema/framework-schema.js";
|
|
2
|
+
export * from "./extractor-api/framework-extractor.js";
|
|
3
|
+
export * from "./extractor-api/extractor-registry.js";
|
|
4
|
+
export * from "./extractors/nestjs-extractor.js";
|
|
5
|
+
export * from "./extractors/react-extractor.js";
|
|
6
|
+
export * from "./extractors/express-extractor.js";
|
|
7
|
+
export * from "./extractors/nextjs-extractor.js";
|
|
8
|
+
export * from "./extractors/angular-extractor.js";
|
|
9
|
+
export * from "./extractors/vue-extractor.js";
|
|
10
|
+
export * from "./extractors/svelte-extractor.js";
|
|
11
|
+
export * from "./extractors/fastify-extractor.js";
|
|
12
|
+
export * from "./extractors/fastapi-extractor.js";
|
|
13
|
+
export * from "./extractors/solid-extractor.js";
|
|
14
|
+
export * from "./extractors/astro-extractor.js";
|
|
15
|
+
export * from "./extractors/django-extractor.js";
|
|
16
|
+
export * from "./extractors/flask-extractor.js";
|
|
17
|
+
export * from "./extractors/spring-extractor.js";
|
|
18
|
+
export * from "./extractors/rails-extractor.js";
|
|
19
|
+
export * from "./extractors/phoenix-extractor.js";
|
|
20
|
+
export * from "./extractors/graphql-extractor.js";
|
|
21
|
+
export * from "./extractors/laravel-extractor.js";
|
|
22
|
+
export * from "./extractors/flutter-extractor.js";
|
|
23
|
+
export * from "./store/framework-store.js";
|
|
24
|
+
export * from "./runner/run-extractors.js";
|
|
25
|
+
export * from "./runner/load-pack-extractors.js";
|
|
26
|
+
export * from "./query/framework-query-api.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { IEdge, INode } from '@shrkcrft/graph';
|
|
2
|
+
import type { IFrameworkSnapshot } from '../schema/framework-schema.js';
|
|
3
|
+
export interface IFrameworkListOptions {
|
|
4
|
+
framework?: string;
|
|
5
|
+
subtype?: string;
|
|
6
|
+
/** Filter by project-relative file path. */
|
|
7
|
+
file?: string;
|
|
8
|
+
/** Cap on returned entities. Default 200. */
|
|
9
|
+
limit?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Read-only query API for the framework snapshot. Merges in-memory
|
|
13
|
+
* with the code-graph snapshot when callers need cross-store joins
|
|
14
|
+
* (e.g. "entities declared in file F"). For the MVP the file
|
|
15
|
+
* relationship is read from the framework store's edges directly.
|
|
16
|
+
*/
|
|
17
|
+
export declare class FrameworkQueryApi {
|
|
18
|
+
private readonly snap;
|
|
19
|
+
private readonly entitiesByFile;
|
|
20
|
+
private readonly entitiesByKey;
|
|
21
|
+
constructor(snap: IFrameworkSnapshot);
|
|
22
|
+
static fromStore(projectRoot: string): FrameworkQueryApi;
|
|
23
|
+
static missingDescription(projectRoot: string): string | undefined;
|
|
24
|
+
manifest(): IFrameworkSnapshot['manifest'];
|
|
25
|
+
/** Entities declared in a specific file. */
|
|
26
|
+
forFile(filePath: string): readonly INode[];
|
|
27
|
+
/** List entities, optionally filtered by framework + subtype + file. */
|
|
28
|
+
list(opts?: IFrameworkListOptions): readonly INode[];
|
|
29
|
+
/** Returns all NestJS routes flattened to `{method, path, handler, file}`. */
|
|
30
|
+
routes(): readonly {
|
|
31
|
+
method: string;
|
|
32
|
+
path: string;
|
|
33
|
+
handler: string;
|
|
34
|
+
file: string;
|
|
35
|
+
}[];
|
|
36
|
+
/** All framework-scoped edges. */
|
|
37
|
+
edges(): readonly IEdge[];
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=framework-query-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framework-query-api.d.ts","sourceRoot":"","sources":["../../src/query/framework-query-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAEpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAExE,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,qBAAa,iBAAiB;IAIhB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHjC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwC;IACvE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAwC;gBAEzC,IAAI,EAAE,kBAAkB;IAkBrD,MAAM,CAAC,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,iBAAiB;IAKxD,MAAM,CAAC,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAKlE,QAAQ,IAAI,kBAAkB,CAAC,UAAU,CAAC;IAI1C,4CAA4C;IAC5C,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,KAAK,EAAE;IAI3C,wEAAwE;IACxE,IAAI,CAAC,IAAI,GAAE,qBAA0B,GAAG,SAAS,KAAK,EAAE;IAoBxD,8EAA8E;IAC9E,MAAM,IAAI,SAAS;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE;IAiBpF,kCAAkC;IAClC,KAAK,IAAI,SAAS,KAAK,EAAE;CAG1B"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { FrameworkStore } from "../store/framework-store.js";
|
|
2
|
+
/**
|
|
3
|
+
* Read-only query API for the framework snapshot. Merges in-memory
|
|
4
|
+
* with the code-graph snapshot when callers need cross-store joins
|
|
5
|
+
* (e.g. "entities declared in file F"). For the MVP the file
|
|
6
|
+
* relationship is read from the framework store's edges directly.
|
|
7
|
+
*/
|
|
8
|
+
export class FrameworkQueryApi {
|
|
9
|
+
snap;
|
|
10
|
+
entitiesByFile;
|
|
11
|
+
entitiesByKey;
|
|
12
|
+
constructor(snap) {
|
|
13
|
+
this.snap = snap;
|
|
14
|
+
const byFile = new Map();
|
|
15
|
+
const byKey = new Map();
|
|
16
|
+
for (const n of snap.nodes.values()) {
|
|
17
|
+
if (n.path) {
|
|
18
|
+
const list = byFile.get(n.path);
|
|
19
|
+
if (list)
|
|
20
|
+
list.push(n);
|
|
21
|
+
else
|
|
22
|
+
byFile.set(n.path, [n]);
|
|
23
|
+
}
|
|
24
|
+
const key = `${n.data?.['framework'] ?? '?'}:${n.data?.['subtype'] ?? '?'}`;
|
|
25
|
+
const klist = byKey.get(key);
|
|
26
|
+
if (klist)
|
|
27
|
+
klist.push(n);
|
|
28
|
+
else
|
|
29
|
+
byKey.set(key, [n]);
|
|
30
|
+
}
|
|
31
|
+
this.entitiesByFile = byFile;
|
|
32
|
+
this.entitiesByKey = byKey;
|
|
33
|
+
}
|
|
34
|
+
static fromStore(projectRoot) {
|
|
35
|
+
const s = new FrameworkStore(projectRoot).loadSnapshot();
|
|
36
|
+
return new FrameworkQueryApi(s);
|
|
37
|
+
}
|
|
38
|
+
static missingDescription(projectRoot) {
|
|
39
|
+
const exists = new FrameworkStore(projectRoot).exists();
|
|
40
|
+
return exists ? undefined : "Framework store missing. Run 'shrk framework index'.";
|
|
41
|
+
}
|
|
42
|
+
manifest() {
|
|
43
|
+
return this.snap.manifest;
|
|
44
|
+
}
|
|
45
|
+
/** Entities declared in a specific file. */
|
|
46
|
+
forFile(filePath) {
|
|
47
|
+
return this.entitiesByFile.get(filePath) ?? [];
|
|
48
|
+
}
|
|
49
|
+
/** List entities, optionally filtered by framework + subtype + file. */
|
|
50
|
+
list(opts = {}) {
|
|
51
|
+
const limit = opts.limit ?? 200;
|
|
52
|
+
if (opts.framework && opts.subtype) {
|
|
53
|
+
const key = `${opts.framework}:${opts.subtype}`;
|
|
54
|
+
const hits = this.entitiesByKey.get(key) ?? [];
|
|
55
|
+
return filterFile(hits, opts.file).slice(0, limit);
|
|
56
|
+
}
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const list of this.entitiesByKey.values()) {
|
|
59
|
+
for (const n of list) {
|
|
60
|
+
if (opts.framework && n.data?.['framework'] !== opts.framework)
|
|
61
|
+
continue;
|
|
62
|
+
if (opts.subtype && n.data?.['subtype'] !== opts.subtype)
|
|
63
|
+
continue;
|
|
64
|
+
if (opts.file && n.path !== opts.file)
|
|
65
|
+
continue;
|
|
66
|
+
out.push(n);
|
|
67
|
+
if (out.length >= limit)
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/** Returns all NestJS routes flattened to `{method, path, handler, file}`. */
|
|
74
|
+
routes() {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const n of this.snap.nodes.values()) {
|
|
77
|
+
if (n.data?.['framework'] !== 'nestjs')
|
|
78
|
+
continue;
|
|
79
|
+
if (n.data?.['subtype'] !== 'route')
|
|
80
|
+
continue;
|
|
81
|
+
out.push({
|
|
82
|
+
method: n.data['method'] ?? '?',
|
|
83
|
+
path: n.data['path'] ?? '/',
|
|
84
|
+
handler: `${n.data['className'] ?? '?'}.${n.data['handler'] ?? '?'}`,
|
|
85
|
+
file: n.path ?? '',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return out.sort((a, b) => a.method.localeCompare(b.method) || a.path.localeCompare(b.path) || a.handler.localeCompare(b.handler));
|
|
89
|
+
}
|
|
90
|
+
/** All framework-scoped edges. */
|
|
91
|
+
edges() {
|
|
92
|
+
return [...this.snap.edges.values()];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function filterFile(nodes, file) {
|
|
96
|
+
if (!file)
|
|
97
|
+
return nodes;
|
|
98
|
+
return nodes.filter((n) => n.path === file);
|
|
99
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { IPackDiscoveryResult } from '@shrkcrft/packs';
|
|
2
|
+
import type { IFrameworkExtractor } from '../extractor-api/framework-extractor.js';
|
|
3
|
+
import { FrameworkExtractorRegistry } from '../extractor-api/extractor-registry.js';
|
|
4
|
+
export interface ILoadPackExtractorsResult {
|
|
5
|
+
/** Extractors loaded successfully. */
|
|
6
|
+
extractors: readonly IFrameworkExtractor[];
|
|
7
|
+
/** Free-form messages per skipped / failed pack. */
|
|
8
|
+
diagnostics: readonly string[];
|
|
9
|
+
/** Packs that contributed at least one extractor. */
|
|
10
|
+
packs: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Walk the pack discovery result and load any
|
|
14
|
+
* `contributions.frameworkExtractorFiles` declared by valid packs.
|
|
15
|
+
*
|
|
16
|
+
* Errors per pack become diagnostics — never propagated. Framework
|
|
17
|
+
* name collisions with built-in extractors (or with each other) are
|
|
18
|
+
* also diagnostics; the colliding contribution is skipped. Built-in
|
|
19
|
+
* extractors always win — packs may not shadow them.
|
|
20
|
+
*
|
|
21
|
+
* Pure: this loader does NOT mutate any registry on its own. The
|
|
22
|
+
* caller decides what to do with the returned extractors (typically
|
|
23
|
+
* `registry.register(ex)` against a `defaultRegistry()`).
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadPackExtractors(discovery: IPackDiscoveryResult, builtinFrameworkNames: ReadonlySet<string>): Promise<ILoadPackExtractorsResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Convenience: build a `FrameworkExtractorRegistry` pre-populated with
|
|
28
|
+
* the built-ins AND with pack-contributed extractors. Diagnostics are
|
|
29
|
+
* available for surfacing in CLI / MCP output via the returned tuple.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildRegistryWithPacks(defaultRegistry: FrameworkExtractorRegistry, discovery: IPackDiscoveryResult): Promise<{
|
|
32
|
+
registry: FrameworkExtractorRegistry;
|
|
33
|
+
diagnostics: readonly string[];
|
|
34
|
+
packs: readonly string[];
|
|
35
|
+
}>;
|
|
36
|
+
//# sourceMappingURL=load-pack-extractors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-pack-extractors.d.ts","sourceRoot":"","sources":["../../src/runner/load-pack-extractors.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AACnF,OAAO,EAAE,0BAA0B,EAAE,MAAM,wCAAwC,CAAC;AAEpF,MAAM,WAAW,yBAAyB;IACxC,sCAAsC;IACtC,UAAU,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC3C,oDAAoD;IACpD,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,qDAAqD;IACrD,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,oBAAoB,EAC/B,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAAC,GACzC,OAAO,CAAC,yBAAyB,CAAC,CAqDpC;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,eAAe,EAAE,0BAA0B,EAC3C,SAAS,EAAE,oBAAoB,GAC9B,OAAO,CAAC;IAAE,QAAQ,EAAE,0BAA0B,CAAC;IAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC,CAa7G"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as nodePath from 'node:path';
|
|
2
|
+
import { safeImport } from '@shrkcrft/core';
|
|
3
|
+
/**
|
|
4
|
+
* Walk the pack discovery result and load any
|
|
5
|
+
* `contributions.frameworkExtractorFiles` declared by valid packs.
|
|
6
|
+
*
|
|
7
|
+
* Errors per pack become diagnostics — never propagated. Framework
|
|
8
|
+
* name collisions with built-in extractors (or with each other) are
|
|
9
|
+
* also diagnostics; the colliding contribution is skipped. Built-in
|
|
10
|
+
* extractors always win — packs may not shadow them.
|
|
11
|
+
*
|
|
12
|
+
* Pure: this loader does NOT mutate any registry on its own. The
|
|
13
|
+
* caller decides what to do with the returned extractors (typically
|
|
14
|
+
* `registry.register(ex)` against a `defaultRegistry()`).
|
|
15
|
+
*/
|
|
16
|
+
export async function loadPackExtractors(discovery, builtinFrameworkNames) {
|
|
17
|
+
const extractors = [];
|
|
18
|
+
const diagnostics = [];
|
|
19
|
+
const packs = [];
|
|
20
|
+
const seenNames = new Set(builtinFrameworkNames);
|
|
21
|
+
for (const pack of discovery.validPacks) {
|
|
22
|
+
const files = pack.manifest?.contributions.frameworkExtractorFiles ?? [];
|
|
23
|
+
if (files.length === 0)
|
|
24
|
+
continue;
|
|
25
|
+
let contributedCount = 0;
|
|
26
|
+
for (const rel of files) {
|
|
27
|
+
const abs = nodePath.resolve(pack.packageRoot, rel);
|
|
28
|
+
const result = await safeImport(abs);
|
|
29
|
+
if (!result.ok) {
|
|
30
|
+
diagnostics.push(`${pack.packageName}:${rel}: load failed (${result.error.message})`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const fromDefault = result.module.default;
|
|
34
|
+
const candidates = [];
|
|
35
|
+
if (Array.isArray(fromDefault)) {
|
|
36
|
+
candidates.push(...fromDefault);
|
|
37
|
+
}
|
|
38
|
+
else if (fromDefault && typeof fromDefault === 'object') {
|
|
39
|
+
candidates.push(fromDefault);
|
|
40
|
+
}
|
|
41
|
+
if (result.module.extractor)
|
|
42
|
+
candidates.push(result.module.extractor);
|
|
43
|
+
if (Array.isArray(result.module.extractors))
|
|
44
|
+
candidates.push(...result.module.extractors);
|
|
45
|
+
if (candidates.length === 0) {
|
|
46
|
+
diagnostics.push(`${pack.packageName}:${rel}: no extractor exports found`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
for (const ex of candidates) {
|
|
50
|
+
if (!ex || typeof ex !== 'object' || typeof ex.framework !== 'string' || typeof ex.fileMatches !== 'function' || typeof ex.extract !== 'function') {
|
|
51
|
+
diagnostics.push(`${pack.packageName}:${rel}: invalid extractor shape (missing framework / fileMatches / extract)`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (seenNames.has(ex.framework)) {
|
|
55
|
+
diagnostics.push(`${pack.packageName}:${rel}: framework "${ex.framework}" already registered — skipping`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
seenNames.add(ex.framework);
|
|
59
|
+
extractors.push(ex);
|
|
60
|
+
contributedCount += 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (contributedCount > 0)
|
|
64
|
+
packs.push(pack.packageName);
|
|
65
|
+
}
|
|
66
|
+
return { extractors, diagnostics, packs };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Convenience: build a `FrameworkExtractorRegistry` pre-populated with
|
|
70
|
+
* the built-ins AND with pack-contributed extractors. Diagnostics are
|
|
71
|
+
* available for surfacing in CLI / MCP output via the returned tuple.
|
|
72
|
+
*/
|
|
73
|
+
export async function buildRegistryWithPacks(defaultRegistry, discovery) {
|
|
74
|
+
const builtinNames = new Set(defaultRegistry.list().map((e) => e.framework));
|
|
75
|
+
const loaded = await loadPackExtractors(discovery, builtinNames);
|
|
76
|
+
for (const ex of loaded.extractors) {
|
|
77
|
+
// `registry.register` throws on collision — we've already filtered
|
|
78
|
+
// duplicates above. Use a try/catch just in case.
|
|
79
|
+
try {
|
|
80
|
+
defaultRegistry.register(ex);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
loaded.diagnostics.concat(`${ex.framework}: register failed (${e.message})`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { registry: defaultRegistry, diagnostics: loaded.diagnostics, packs: loaded.packs };
|
|
87
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { FrameworkExtractorRegistry } from '../extractor-api/extractor-registry.js';
|
|
2
|
+
import { type IFrameworkManifest } from '../schema/framework-schema.js';
|
|
3
|
+
export interface IRunExtractorsOptions {
|
|
4
|
+
projectRoot: string;
|
|
5
|
+
/** Custom registry (e.g. with pack-contributed extractors). Default: built-ins. */
|
|
6
|
+
registry?: FrameworkExtractorRegistry;
|
|
7
|
+
/** Restrict to a subset of frameworks (by name). */
|
|
8
|
+
only?: readonly string[];
|
|
9
|
+
/** Cap on files scanned (for tests). 0 = no cap. */
|
|
10
|
+
maxFiles?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface IRunExtractorsResult {
|
|
13
|
+
manifest: IFrameworkManifest;
|
|
14
|
+
durationMs: number;
|
|
15
|
+
filesScanned: number;
|
|
16
|
+
diagnostics: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
/** Built-in registry pre-populated with the bundled extractors. */
|
|
19
|
+
export declare function defaultRegistry(): FrameworkExtractorRegistry;
|
|
20
|
+
/**
|
|
21
|
+
* Walk the project's source files (via the code graph snapshot) and
|
|
22
|
+
* run every applicable framework extractor. Writes results to the
|
|
23
|
+
* framework store.
|
|
24
|
+
*
|
|
25
|
+
* The code graph must already exist — frameworks are *enriched* views
|
|
26
|
+
* over the graph, not a from-scratch index.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runExtractors(options: IRunExtractorsOptions): IRunExtractorsResult;
|
|
29
|
+
//# sourceMappingURL=run-extractors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-extractors.d.ts","sourceRoot":"","sources":["../../src/runner/run-extractors.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,0BAA0B,EAAE,MAAM,wCAAwC,CAAC;AAqBpF,OAAO,EACL,KAAK,kBAAkB,EACxB,MAAM,+BAA+B,CAAC;AAIvC,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,mFAAmF;IACnF,QAAQ,CAAC,EAAE,0BAA0B,CAAC;IACtC,oDAAoD;IACpD,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,mEAAmE;AACnE,wBAAgB,eAAe,IAAI,0BAA0B,CAsB5D;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,qBAAqB,GAAG,oBAAoB,CAoElF"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { GraphQueryApi, GraphStore } from '@shrkcrft/graph';
|
|
3
|
+
import { FrameworkExtractorRegistry } from "../extractor-api/extractor-registry.js";
|
|
4
|
+
import { angularExtractor } from "../extractors/angular-extractor.js";
|
|
5
|
+
import { astroExtractor } from "../extractors/astro-extractor.js";
|
|
6
|
+
import { djangoExtractor } from "../extractors/django-extractor.js";
|
|
7
|
+
import { expressExtractor } from "../extractors/express-extractor.js";
|
|
8
|
+
import { fastapiExtractor } from "../extractors/fastapi-extractor.js";
|
|
9
|
+
import { fastifyExtractor } from "../extractors/fastify-extractor.js";
|
|
10
|
+
import { flaskExtractor } from "../extractors/flask-extractor.js";
|
|
11
|
+
import { graphqlExtractor } from "../extractors/graphql-extractor.js";
|
|
12
|
+
import { flutterExtractor } from "../extractors/flutter-extractor.js";
|
|
13
|
+
import { laravelExtractor } from "../extractors/laravel-extractor.js";
|
|
14
|
+
import { nestjsExtractor } from "../extractors/nestjs-extractor.js";
|
|
15
|
+
import { nextjsExtractor } from "../extractors/nextjs-extractor.js";
|
|
16
|
+
import { reactExtractor } from "../extractors/react-extractor.js";
|
|
17
|
+
import { phoenixExtractor } from "../extractors/phoenix-extractor.js";
|
|
18
|
+
import { railsExtractor } from "../extractors/rails-extractor.js";
|
|
19
|
+
import { solidExtractor } from "../extractors/solid-extractor.js";
|
|
20
|
+
import { springExtractor } from "../extractors/spring-extractor.js";
|
|
21
|
+
import { svelteExtractor } from "../extractors/svelte-extractor.js";
|
|
22
|
+
import { vueExtractor } from "../extractors/vue-extractor.js";
|
|
23
|
+
import { FrameworkStore } from "../store/framework-store.js";
|
|
24
|
+
import * as nodePath from 'node:path';
|
|
25
|
+
/** Built-in registry pre-populated with the bundled extractors. */
|
|
26
|
+
export function defaultRegistry() {
|
|
27
|
+
const r = new FrameworkExtractorRegistry();
|
|
28
|
+
r.register(nestjsExtractor);
|
|
29
|
+
r.register(reactExtractor);
|
|
30
|
+
r.register(expressExtractor);
|
|
31
|
+
r.register(nextjsExtractor);
|
|
32
|
+
r.register(angularExtractor);
|
|
33
|
+
r.register(vueExtractor);
|
|
34
|
+
r.register(svelteExtractor);
|
|
35
|
+
r.register(fastifyExtractor);
|
|
36
|
+
r.register(fastapiExtractor);
|
|
37
|
+
r.register(solidExtractor);
|
|
38
|
+
r.register(astroExtractor);
|
|
39
|
+
r.register(djangoExtractor);
|
|
40
|
+
r.register(flaskExtractor);
|
|
41
|
+
r.register(springExtractor);
|
|
42
|
+
r.register(railsExtractor);
|
|
43
|
+
r.register(phoenixExtractor);
|
|
44
|
+
r.register(graphqlExtractor);
|
|
45
|
+
r.register(laravelExtractor);
|
|
46
|
+
r.register(flutterExtractor);
|
|
47
|
+
return r;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Walk the project's source files (via the code graph snapshot) and
|
|
51
|
+
* run every applicable framework extractor. Writes results to the
|
|
52
|
+
* framework store.
|
|
53
|
+
*
|
|
54
|
+
* The code graph must already exist — frameworks are *enriched* views
|
|
55
|
+
* over the graph, not a from-scratch index.
|
|
56
|
+
*/
|
|
57
|
+
export function runExtractors(options) {
|
|
58
|
+
const start = Date.now();
|
|
59
|
+
const diagnostics = [];
|
|
60
|
+
const graphStore = new GraphStore(options.projectRoot);
|
|
61
|
+
if (!graphStore.exists()) {
|
|
62
|
+
throw new Error("code-graph store missing. Run 'shrk graph index' before 'shrk framework index'.");
|
|
63
|
+
}
|
|
64
|
+
const api = GraphQueryApi.fromStore(options.projectRoot);
|
|
65
|
+
const registry = options.registry ?? defaultRegistry();
|
|
66
|
+
const enabled = filterEnabled(registry, options.only);
|
|
67
|
+
const aggregateNodes = [];
|
|
68
|
+
const aggregateEdges = [];
|
|
69
|
+
const seenNodeIds = new Set();
|
|
70
|
+
const seenEdgeIds = new Set();
|
|
71
|
+
let filesScanned = 0;
|
|
72
|
+
for (const file of api.allFiles()) {
|
|
73
|
+
if (options.maxFiles && options.maxFiles > 0 && filesScanned >= options.maxFiles)
|
|
74
|
+
break;
|
|
75
|
+
if (!file.path)
|
|
76
|
+
continue;
|
|
77
|
+
filesScanned += 1;
|
|
78
|
+
let content;
|
|
79
|
+
try {
|
|
80
|
+
content = readFileSync(nodePath.resolve(options.projectRoot, file.path), 'utf8');
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
for (const ex of enabled) {
|
|
86
|
+
if (!safeMatches(ex, file.path, content, diagnostics))
|
|
87
|
+
continue;
|
|
88
|
+
try {
|
|
89
|
+
const result = ex.extract({ filePath: file.path, content, fileNodeId: file.id });
|
|
90
|
+
for (const n of result.nodes) {
|
|
91
|
+
if (seenNodeIds.has(n.id))
|
|
92
|
+
continue;
|
|
93
|
+
seenNodeIds.add(n.id);
|
|
94
|
+
aggregateNodes.push(n);
|
|
95
|
+
}
|
|
96
|
+
for (const e of result.edges) {
|
|
97
|
+
if (seenEdgeIds.has(e.id))
|
|
98
|
+
continue;
|
|
99
|
+
seenEdgeIds.add(e.id);
|
|
100
|
+
aggregateEdges.push(e);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
diagnostics.push(`${ex.framework}: ${file.path}: ${err.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const countsByFramework = {};
|
|
109
|
+
const countsBySubtype = {};
|
|
110
|
+
for (const n of aggregateNodes) {
|
|
111
|
+
const framework = String(n.data?.['framework'] ?? 'unknown');
|
|
112
|
+
const subtype = String(n.data?.['subtype'] ?? 'unknown');
|
|
113
|
+
countsByFramework[framework] = (countsByFramework[framework] ?? 0) + 1;
|
|
114
|
+
const key = `${framework}:${subtype}`;
|
|
115
|
+
countsBySubtype[key] = (countsBySubtype[key] ?? 0) + 1;
|
|
116
|
+
}
|
|
117
|
+
const frameworks = [...new Set(enabled.map((e) => e.framework))].sort();
|
|
118
|
+
const store = new FrameworkStore(options.projectRoot);
|
|
119
|
+
const manifest = store.writeSnapshot(aggregateNodes, aggregateEdges, {
|
|
120
|
+
projectRoot: options.projectRoot,
|
|
121
|
+
lastBuiltAt: new Date().toISOString(),
|
|
122
|
+
lastBuildDurationMs: Date.now() - start,
|
|
123
|
+
countsByFramework,
|
|
124
|
+
countsBySubtype,
|
|
125
|
+
frameworks,
|
|
126
|
+
});
|
|
127
|
+
return { manifest, durationMs: Date.now() - start, filesScanned, diagnostics };
|
|
128
|
+
}
|
|
129
|
+
function filterEnabled(registry, only) {
|
|
130
|
+
const all = registry.list();
|
|
131
|
+
if (!only || only.length === 0)
|
|
132
|
+
return all;
|
|
133
|
+
const set = new Set(only);
|
|
134
|
+
return all.filter((e) => set.has(e.framework));
|
|
135
|
+
}
|
|
136
|
+
function safeMatches(ex, path, content, diagnostics) {
|
|
137
|
+
try {
|
|
138
|
+
return ex.fileMatches({ path, content });
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
diagnostics.push(`${ex.framework}: fileMatches threw on ${path}: ${e.message}`);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|