@shrkcrft/graph 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/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/indexer/detect-workspace.d.ts +18 -0
- package/dist/indexer/detect-workspace.d.ts.map +1 -0
- package/dist/indexer/detect-workspace.js +80 -0
- package/dist/indexer/extract-csharp-file.d.ts +27 -0
- package/dist/indexer/extract-csharp-file.d.ts.map +1 -0
- package/dist/indexer/extract-csharp-file.js +163 -0
- package/dist/indexer/extract-dart-file.d.ts +28 -0
- package/dist/indexer/extract-dart-file.d.ts.map +1 -0
- package/dist/indexer/extract-dart-file.js +167 -0
- package/dist/indexer/extract-elixir-file.d.ts +27 -0
- package/dist/indexer/extract-elixir-file.d.ts.map +1 -0
- package/dist/indexer/extract-elixir-file.js +164 -0
- package/dist/indexer/extract-go-file.d.ts +28 -0
- package/dist/indexer/extract-go-file.d.ts.map +1 -0
- package/dist/indexer/extract-go-file.js +156 -0
- package/dist/indexer/extract-java-file.d.ts +25 -0
- package/dist/indexer/extract-java-file.d.ts.map +1 -0
- package/dist/indexer/extract-java-file.js +140 -0
- package/dist/indexer/extract-kotlin-file.d.ts +20 -0
- package/dist/indexer/extract-kotlin-file.d.ts.map +1 -0
- package/dist/indexer/extract-kotlin-file.js +158 -0
- package/dist/indexer/extract-php-file.d.ts +26 -0
- package/dist/indexer/extract-php-file.d.ts.map +1 -0
- package/dist/indexer/extract-php-file.js +161 -0
- package/dist/indexer/extract-python-file.d.ts +30 -0
- package/dist/indexer/extract-python-file.d.ts.map +1 -0
- package/dist/indexer/extract-python-file.js +196 -0
- package/dist/indexer/extract-ruby-file.d.ts +29 -0
- package/dist/indexer/extract-ruby-file.d.ts.map +1 -0
- package/dist/indexer/extract-ruby-file.js +151 -0
- package/dist/indexer/extract-rust-file.d.ts +27 -0
- package/dist/indexer/extract-rust-file.d.ts.map +1 -0
- package/dist/indexer/extract-rust-file.js +186 -0
- package/dist/indexer/extract-swift-file.d.ts +27 -0
- package/dist/indexer/extract-swift-file.d.ts.map +1 -0
- package/dist/indexer/extract-swift-file.js +168 -0
- package/dist/indexer/extract-ts-file.d.ts +79 -0
- package/dist/indexer/extract-ts-file.d.ts.map +1 -0
- package/dist/indexer/extract-ts-file.js +403 -0
- package/dist/indexer/incremental-updater.d.ts +41 -0
- package/dist/indexer/incremental-updater.d.ts.map +1 -0
- package/dist/indexer/incremental-updater.js +395 -0
- package/dist/indexer/index-builder.d.ts +23 -0
- package/dist/indexer/index-builder.d.ts.map +1 -0
- package/dist/indexer/index-builder.js +289 -0
- package/dist/indexer/resolve-imports.d.ts +36 -0
- package/dist/indexer/resolve-imports.d.ts.map +1 -0
- package/dist/indexer/resolve-imports.js +144 -0
- package/dist/indexer/unresolved-imports.d.ts +20 -0
- package/dist/indexer/unresolved-imports.d.ts.map +1 -0
- package/dist/indexer/unresolved-imports.js +32 -0
- package/dist/query/cycle-detection.d.ts +40 -0
- package/dist/query/cycle-detection.d.ts.map +1 -0
- package/dist/query/cycle-detection.js +135 -0
- package/dist/query/query-api.d.ts +87 -0
- package/dist/query/query-api.d.ts.map +1 -0
- package/dist/query/query-api.js +232 -0
- package/dist/schema/edge-kind.d.ts +31 -0
- package/dist/schema/edge-kind.d.ts.map +1 -0
- package/dist/schema/edge-kind.js +35 -0
- package/dist/schema/edge.d.ts +22 -0
- package/dist/schema/edge.d.ts.map +1 -0
- package/dist/schema/edge.js +1 -0
- package/dist/schema/file-fingerprint.d.ts +22 -0
- package/dist/schema/file-fingerprint.d.ts.map +1 -0
- package/dist/schema/file-fingerprint.js +1 -0
- package/dist/schema/graph-snapshot.d.ts +18 -0
- package/dist/schema/graph-snapshot.d.ts.map +1 -0
- package/dist/schema/graph-snapshot.js +1 -0
- package/dist/schema/manifest.d.ts +47 -0
- package/dist/schema/manifest.d.ts.map +1 -0
- package/dist/schema/manifest.js +1 -0
- package/dist/schema/node-kind.d.ts +21 -0
- package/dist/schema/node-kind.d.ts.map +1 -0
- package/dist/schema/node-kind.js +27 -0
- package/dist/schema/node.d.ts +26 -0
- package/dist/schema/node.d.ts.map +1 -0
- package/dist/schema/node.js +1 -0
- package/dist/schema/schema-version.d.ts +10 -0
- package/dist/schema/schema-version.d.ts.map +1 -0
- package/dist/schema/schema-version.js +8 -0
- package/dist/store/file-fingerprint.d.ts +8 -0
- package/dist/store/file-fingerprint.d.ts.map +1 -0
- package/dist/store/file-fingerprint.js +64 -0
- package/dist/store/graph-store.d.ts +48 -0
- package/dist/store/graph-store.d.ts.map +1 -0
- package/dist/store/graph-store.js +194 -0
- package/package.json +54 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import * as nodePath from 'node:path';
|
|
4
|
+
import * as ts from 'typescript';
|
|
5
|
+
import { buildSymbolIndex, SymbolVisibility, } from '@shrkcrft/inspector';
|
|
6
|
+
import { EdgeKind } from "../schema/edge-kind.js";
|
|
7
|
+
import { NodeKind } from "../schema/node-kind.js";
|
|
8
|
+
export const EXTRACT_TS_FILE_SOURCE = 'extract-ts-file@v1';
|
|
9
|
+
/**
|
|
10
|
+
* Per-file import scan. The boundaries package owns the project-wide
|
|
11
|
+
* `scanImports`; for the graph extractor we need a per-file pass that
|
|
12
|
+
* runs against a buffer we already have in hand. The regexes mirror
|
|
13
|
+
* `boundaries/scan-imports.ts` deliberately — keeping them in sync is a
|
|
14
|
+
* test obligation, not a runtime one.
|
|
15
|
+
*/
|
|
16
|
+
const IMPORT_RE = /(?:^|\s)(?:import|export)\s+[^'"`]*?from\s+['"]([^'"`]+)['"]/g;
|
|
17
|
+
const SIDE_EFFECT_IMPORT_RE = /(?:^|\s)import\s+['"]([^'"`]+)['"]/g;
|
|
18
|
+
const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*['"]([^'"`]+)['"]\s*\)/g;
|
|
19
|
+
const REQUIRE_RE = /\brequire\s*\(\s*['"]([^'"`]+)['"]\s*\)/g;
|
|
20
|
+
/**
|
|
21
|
+
* Extract graph entities from a single TS/TSX/JS/JSX file.
|
|
22
|
+
*
|
|
23
|
+
* Re-uses `buildSymbolIndex` for symbol detection (per-file AST, no full
|
|
24
|
+
* program). Import edges carry the literal specifier; resolution to the
|
|
25
|
+
* target file id happens in `resolve-imports.ts` (R64).
|
|
26
|
+
*/
|
|
27
|
+
export function extractTsFile(fingerprint, absPath, content) {
|
|
28
|
+
const text = content ?? readFileSync(absPath, 'utf8');
|
|
29
|
+
// Single-file-component languages (Vue, Svelte, Astro) are not pure TS.
|
|
30
|
+
// We produce a minimal File node and let `@shrkcrft/framework-scanners`
|
|
31
|
+
// detect component-level structure. Imports still extracted from
|
|
32
|
+
// `<script>` blocks via the regex pass below.
|
|
33
|
+
if (fingerprint.language === 'vue' ||
|
|
34
|
+
fingerprint.language === 'svelte' ||
|
|
35
|
+
fingerprint.language === 'astro' ||
|
|
36
|
+
fingerprint.language === 'graphql') {
|
|
37
|
+
const fileNode = makeFileNodeForNonTs(fingerprint);
|
|
38
|
+
return {
|
|
39
|
+
fileNode,
|
|
40
|
+
symbolNodes: [],
|
|
41
|
+
edges: [],
|
|
42
|
+
rawImportSpecifiers: fingerprint.language === 'graphql' ? [] : scanFileImports(text),
|
|
43
|
+
importBindings: [],
|
|
44
|
+
identifierReferences: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const idx = buildSymbolIndex(absPath, text);
|
|
48
|
+
const fileNode = makeFileNode(fingerprint, idx);
|
|
49
|
+
const symbolNodes = [];
|
|
50
|
+
const edges = [];
|
|
51
|
+
for (const e of idx.exports) {
|
|
52
|
+
const sym = makeSymbolNode(fingerprint, e.name, e.kind, e.visibility, e.line);
|
|
53
|
+
symbolNodes.push(sym);
|
|
54
|
+
edges.push(buildEdge(fileNode.id, sym.id, EdgeKind.DeclaresSymbol, {
|
|
55
|
+
visibility: e.visibility,
|
|
56
|
+
declKind: e.kind,
|
|
57
|
+
line: e.line,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
for (const l of idx.locals) {
|
|
61
|
+
const sym = makeSymbolNode(fingerprint, l.name, l.kind, l.visibility, l.line);
|
|
62
|
+
symbolNodes.push(sym);
|
|
63
|
+
edges.push(buildEdge(fileNode.id, sym.id, EdgeKind.DeclaresSymbol, {
|
|
64
|
+
visibility: l.visibility,
|
|
65
|
+
declKind: l.kind,
|
|
66
|
+
line: l.line,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
for (const re of idx.reExports) {
|
|
70
|
+
// The target file is unresolved at extract-time; we record the spec.
|
|
71
|
+
// The indexer post-pass adds the resolved symbol edge once imports
|
|
72
|
+
// resolve. The placeholder symbol id below intentionally has no
|
|
73
|
+
// matching node — the resolver replaces it.
|
|
74
|
+
const placeholderTarget = `symbol:unresolved:${re.from}#${re.name}`;
|
|
75
|
+
edges.push(buildEdge(fileNode.id, placeholderTarget, EdgeKind.ReExportsSymbol, {
|
|
76
|
+
specifier: re.from,
|
|
77
|
+
name: re.name,
|
|
78
|
+
star: re.star,
|
|
79
|
+
line: re.line,
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
const rawImportSpecifiers = scanFileImports(text);
|
|
83
|
+
const { importBindings, identifierReferences } = walkAst(absPath, text);
|
|
84
|
+
return {
|
|
85
|
+
fileNode,
|
|
86
|
+
symbolNodes,
|
|
87
|
+
edges,
|
|
88
|
+
rawImportSpecifiers,
|
|
89
|
+
importBindings,
|
|
90
|
+
identifierReferences,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function pickScriptKind(ext) {
|
|
94
|
+
switch (ext) {
|
|
95
|
+
case '.tsx': return ts.ScriptKind.TSX;
|
|
96
|
+
case '.jsx': return ts.ScriptKind.JSX;
|
|
97
|
+
case '.js':
|
|
98
|
+
case '.mjs':
|
|
99
|
+
case '.cjs': return ts.ScriptKind.JS;
|
|
100
|
+
default: return ts.ScriptKind.TS;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* AST walk that collects:
|
|
105
|
+
* - Named + default import bindings (skip namespace + type-only).
|
|
106
|
+
* - Identifier references in the file body, flagged as `isCall` when
|
|
107
|
+
* the identifier is the callee of a `CallExpression`.
|
|
108
|
+
*
|
|
109
|
+
* Identifiers inside import declarations themselves are not collected —
|
|
110
|
+
* they're declaration sites, not uses.
|
|
111
|
+
*/
|
|
112
|
+
function walkAst(absPath, text) {
|
|
113
|
+
const ext = nodePath.extname(absPath).toLowerCase();
|
|
114
|
+
let sf;
|
|
115
|
+
try {
|
|
116
|
+
sf = ts.createSourceFile(absPath, text, ts.ScriptTarget.Latest, true, pickScriptKind(ext));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { importBindings: [], identifierReferences: [] };
|
|
120
|
+
}
|
|
121
|
+
const bindings = [];
|
|
122
|
+
const refs = [];
|
|
123
|
+
for (const stmt of sf.statements) {
|
|
124
|
+
if (!ts.isImportDeclaration(stmt))
|
|
125
|
+
continue;
|
|
126
|
+
if (!ts.isStringLiteral(stmt.moduleSpecifier))
|
|
127
|
+
continue;
|
|
128
|
+
const specifier = stmt.moduleSpecifier.text;
|
|
129
|
+
const clause = stmt.importClause;
|
|
130
|
+
if (!clause)
|
|
131
|
+
continue;
|
|
132
|
+
if (clause.isTypeOnly)
|
|
133
|
+
continue;
|
|
134
|
+
if (clause.name) {
|
|
135
|
+
bindings.push({
|
|
136
|
+
localName: clause.name.text,
|
|
137
|
+
importedName: 'default',
|
|
138
|
+
specifier,
|
|
139
|
+
isDefault: true,
|
|
140
|
+
line: lineOf(sf, clause.name),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
144
|
+
for (const elem of clause.namedBindings.elements) {
|
|
145
|
+
if (elem.isTypeOnly)
|
|
146
|
+
continue;
|
|
147
|
+
bindings.push({
|
|
148
|
+
localName: elem.name.text,
|
|
149
|
+
importedName: elem.propertyName ? elem.propertyName.text : elem.name.text,
|
|
150
|
+
specifier,
|
|
151
|
+
isDefault: false,
|
|
152
|
+
line: lineOf(sf, elem),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// NamespaceImport (`import * as X`) intentionally skipped.
|
|
157
|
+
}
|
|
158
|
+
function visit(node) {
|
|
159
|
+
// Skip the declaration sites we already harvested.
|
|
160
|
+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node))
|
|
161
|
+
return;
|
|
162
|
+
if (ts.isIdentifier(node) && !isDeclarationName(node)) {
|
|
163
|
+
refs.push({
|
|
164
|
+
name: node.text,
|
|
165
|
+
line: lineOf(sf, node),
|
|
166
|
+
isCall: isCallCallee(node),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
ts.forEachChild(node, visit);
|
|
170
|
+
}
|
|
171
|
+
ts.forEachChild(sf, visit);
|
|
172
|
+
// De-dupe identical (name, line, isCall) triples; a single AST often
|
|
173
|
+
// visits the same identifier twice (e.g. in computed property names).
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
const dedupedRefs = [];
|
|
176
|
+
for (const r of refs) {
|
|
177
|
+
const k = `${r.name}|${r.line}|${r.isCall ? 1 : 0}`;
|
|
178
|
+
if (seen.has(k))
|
|
179
|
+
continue;
|
|
180
|
+
seen.add(k);
|
|
181
|
+
dedupedRefs.push(r);
|
|
182
|
+
}
|
|
183
|
+
return { importBindings: bindings, identifierReferences: dedupedRefs };
|
|
184
|
+
}
|
|
185
|
+
function isDeclarationName(id) {
|
|
186
|
+
const parent = id.parent;
|
|
187
|
+
if (!parent)
|
|
188
|
+
return false;
|
|
189
|
+
// The `name` of various declarations; we don't want to record those.
|
|
190
|
+
if ((ts.isVariableDeclaration(parent) ||
|
|
191
|
+
ts.isFunctionDeclaration(parent) ||
|
|
192
|
+
ts.isClassDeclaration(parent) ||
|
|
193
|
+
ts.isInterfaceDeclaration(parent) ||
|
|
194
|
+
ts.isTypeAliasDeclaration(parent) ||
|
|
195
|
+
ts.isEnumDeclaration(parent) ||
|
|
196
|
+
ts.isParameter(parent) ||
|
|
197
|
+
ts.isPropertyDeclaration(parent) ||
|
|
198
|
+
ts.isPropertySignature(parent) ||
|
|
199
|
+
ts.isMethodDeclaration(parent) ||
|
|
200
|
+
ts.isMethodSignature(parent) ||
|
|
201
|
+
ts.isEnumMember(parent) ||
|
|
202
|
+
ts.isBindingElement(parent)) &&
|
|
203
|
+
parent.name === id) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
// Property accesses: `foo.bar` — `bar` is a property name, not a free
|
|
207
|
+
// identifier. We DO want to capture `foo`.
|
|
208
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === id)
|
|
209
|
+
return true;
|
|
210
|
+
if (ts.isQualifiedName(parent) && parent.right === id)
|
|
211
|
+
return true;
|
|
212
|
+
// Property assignment in object literal: `{ bar: x }` — `bar` is a key.
|
|
213
|
+
if (ts.isPropertyAssignment(parent) && parent.name === id)
|
|
214
|
+
return true;
|
|
215
|
+
if (ts.isShorthandPropertyAssignment(parent) && parent.name === id) {
|
|
216
|
+
// `{ foo }` — `foo` is BOTH key and value; we want it.
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
function isCallCallee(id) {
|
|
222
|
+
const parent = id.parent;
|
|
223
|
+
if (!parent)
|
|
224
|
+
return false;
|
|
225
|
+
if (ts.isCallExpression(parent) && parent.expression === id)
|
|
226
|
+
return true;
|
|
227
|
+
// `new Foo(...)` — semantically an invocation; count it as a call.
|
|
228
|
+
if (ts.isNewExpression(parent) && parent.expression === id)
|
|
229
|
+
return true;
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
function lineOf(sf, node) {
|
|
233
|
+
return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Stitch identifier references + import bindings into edges.
|
|
237
|
+
*
|
|
238
|
+
* Called by the indexer after per-file extraction + import resolution.
|
|
239
|
+
* Emits `references-symbol` / `calls-symbol` edges from a file to the
|
|
240
|
+
* symbol(s) it uses. Same-file references resolve against the file's
|
|
241
|
+
* own declared symbols; cross-file references resolve via the import
|
|
242
|
+
* bindings + the resolver's spec → targetPath map.
|
|
243
|
+
*
|
|
244
|
+
* Default imports target `symbol:<targetPath>#<defaultExportName>`
|
|
245
|
+
* when the default export name is known (via the file node's
|
|
246
|
+
* `defaultExportName` data), or `#default` as a placeholder when not.
|
|
247
|
+
*/
|
|
248
|
+
export function stitchPerFileReferences(input) {
|
|
249
|
+
const { fileNodeId, extracted, resolvedSpec, defaultExportNameByPath, localSymbolNamesInThisFile } = input;
|
|
250
|
+
const bindings = new Map();
|
|
251
|
+
for (const b of extracted.importBindings) {
|
|
252
|
+
const targetPath = resolvedSpec.get(b.specifier);
|
|
253
|
+
if (!targetPath)
|
|
254
|
+
continue;
|
|
255
|
+
if (b.isDefault) {
|
|
256
|
+
const defName = defaultExportNameByPath.get(targetPath);
|
|
257
|
+
bindings.set(b.localName, `symbol:${targetPath}#${defName ?? 'default'}`);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
bindings.set(b.localName, `symbol:${targetPath}#${b.importedName}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const out = [];
|
|
264
|
+
const seen = new Set();
|
|
265
|
+
for (const r of extracted.identifierReferences) {
|
|
266
|
+
let target = bindings.get(r.name);
|
|
267
|
+
if (!target)
|
|
268
|
+
target = localSymbolNamesInThisFile.get(r.name);
|
|
269
|
+
if (!target)
|
|
270
|
+
continue;
|
|
271
|
+
if (target === fileNodeId)
|
|
272
|
+
continue; // ignore self-loops
|
|
273
|
+
const kind = r.isCall ? EdgeKind.CallsSymbol : EdgeKind.ReferencesSymbol;
|
|
274
|
+
// De-dupe (target, kind) — many call sites of the same function on
|
|
275
|
+
// different lines collapse to a single file-level edge. We keep
|
|
276
|
+
// line info via the data of the first occurrence; the schema is
|
|
277
|
+
// ready for symbol-level edges in Wave 3 proper.
|
|
278
|
+
const edgeKey = `${target}|${kind}`;
|
|
279
|
+
if (seen.has(edgeKey))
|
|
280
|
+
continue;
|
|
281
|
+
seen.add(edgeKey);
|
|
282
|
+
out.push(buildEdge(fileNodeId, target, kind, { line: r.line }));
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
function makeFileNodeForNonTs(fp) {
|
|
287
|
+
const label = fp.path.split('/').pop() ?? fp.path;
|
|
288
|
+
const tags = [fp.language];
|
|
289
|
+
if (isTestPath(fp.path))
|
|
290
|
+
tags.push('test');
|
|
291
|
+
return {
|
|
292
|
+
id: fp.nodeId,
|
|
293
|
+
kind: NodeKind.File,
|
|
294
|
+
label,
|
|
295
|
+
path: fp.path,
|
|
296
|
+
tags,
|
|
297
|
+
data: {
|
|
298
|
+
language: fp.language,
|
|
299
|
+
sizeBytes: fp.sizeBytes,
|
|
300
|
+
sha1: fp.sha1,
|
|
301
|
+
hasDefaultExport: false,
|
|
302
|
+
exportCount: 0,
|
|
303
|
+
localCount: 0,
|
|
304
|
+
reExportCount: 0,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function makeFileNode(fp, idx) {
|
|
309
|
+
const label = fp.path.split('/').pop() ?? fp.path;
|
|
310
|
+
const tags = [];
|
|
311
|
+
if (isTestPath(fp.path))
|
|
312
|
+
tags.push('test');
|
|
313
|
+
if (isGenerated(idx))
|
|
314
|
+
tags.push('generated');
|
|
315
|
+
return {
|
|
316
|
+
id: fp.nodeId,
|
|
317
|
+
kind: NodeKind.File,
|
|
318
|
+
label,
|
|
319
|
+
path: fp.path,
|
|
320
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
321
|
+
data: {
|
|
322
|
+
language: fp.language,
|
|
323
|
+
sizeBytes: fp.sizeBytes,
|
|
324
|
+
sha1: fp.sha1,
|
|
325
|
+
hasDefaultExport: idx.hasDefaultExport,
|
|
326
|
+
...(idx.defaultExportName ? { defaultExportName: idx.defaultExportName } : {}),
|
|
327
|
+
exportCount: idx.exports.length,
|
|
328
|
+
localCount: idx.locals.length,
|
|
329
|
+
reExportCount: idx.reExports.length,
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function makeSymbolNode(fp, name, declKind, visibility, line) {
|
|
334
|
+
return {
|
|
335
|
+
id: `symbol:${fp.path}#${name}`,
|
|
336
|
+
kind: NodeKind.Symbol,
|
|
337
|
+
label: name,
|
|
338
|
+
path: fp.path,
|
|
339
|
+
line,
|
|
340
|
+
data: {
|
|
341
|
+
declKind,
|
|
342
|
+
visibility,
|
|
343
|
+
isExported: visibility === SymbolVisibility.Export || visibility === SymbolVisibility.Default,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function buildEdge(from, to, kind, data) {
|
|
348
|
+
const id = createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex');
|
|
349
|
+
return {
|
|
350
|
+
id,
|
|
351
|
+
from,
|
|
352
|
+
to,
|
|
353
|
+
kind,
|
|
354
|
+
source: EXTRACT_TS_FILE_SOURCE,
|
|
355
|
+
...(data ? { data } : {}),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function isTestPath(rel) {
|
|
359
|
+
return /(?:^|\/)(?:__tests__|__mocks__)\//.test(rel) || /\.(?:test|spec)\.[tj]sx?$/.test(rel);
|
|
360
|
+
}
|
|
361
|
+
function isGenerated(idx) {
|
|
362
|
+
// Symbol-index doesn't preserve raw text; the file-level header check
|
|
363
|
+
// happens in the indexer where the buffer is in hand. Stays false here
|
|
364
|
+
// and is corrected by the indexer when needed.
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
function scanFileImports(text) {
|
|
368
|
+
const out = [];
|
|
369
|
+
collect(text, IMPORT_RE, 'static', out);
|
|
370
|
+
collect(text, SIDE_EFFECT_IMPORT_RE, 'side-effect', out);
|
|
371
|
+
collect(text, DYNAMIC_IMPORT_RE, 'dynamic', out);
|
|
372
|
+
collect(text, REQUIRE_RE, 'require', out);
|
|
373
|
+
// Dedupe identical (specifier, line, kind).
|
|
374
|
+
const seen = new Set();
|
|
375
|
+
const deduped = [];
|
|
376
|
+
for (const it of out) {
|
|
377
|
+
const k = `${it.kind}|${it.specifier}|${it.line}`;
|
|
378
|
+
if (seen.has(k))
|
|
379
|
+
continue;
|
|
380
|
+
seen.add(k);
|
|
381
|
+
deduped.push(it);
|
|
382
|
+
}
|
|
383
|
+
return deduped;
|
|
384
|
+
}
|
|
385
|
+
function collect(text, re, kind, out) {
|
|
386
|
+
re.lastIndex = 0;
|
|
387
|
+
let m;
|
|
388
|
+
while ((m = re.exec(text)) !== null) {
|
|
389
|
+
const specifier = m[1];
|
|
390
|
+
if (!specifier)
|
|
391
|
+
continue;
|
|
392
|
+
const line = lineFromOffset(text, m.index);
|
|
393
|
+
out.push({ specifier, line, kind });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function lineFromOffset(text, offset) {
|
|
397
|
+
let line = 1;
|
|
398
|
+
for (let i = 0; i < offset && i < text.length; i++) {
|
|
399
|
+
if (text.charCodeAt(i) === 10)
|
|
400
|
+
line += 1;
|
|
401
|
+
}
|
|
402
|
+
return line;
|
|
403
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { IGraphManifest } from '../schema/manifest.js';
|
|
2
|
+
export interface IIncrementalUpdateOptions {
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
/** Project-relative file paths (POSIX). */
|
|
5
|
+
changedFiles?: readonly string[];
|
|
6
|
+
/** Project-relative file paths (POSIX). */
|
|
7
|
+
deletedFiles?: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
export interface IIncrementalUpdateResult {
|
|
10
|
+
manifest: IGraphManifest;
|
|
11
|
+
durationMs: number;
|
|
12
|
+
/** Files actually re-extracted (skipped == fingerprint unchanged). */
|
|
13
|
+
updated: readonly string[];
|
|
14
|
+
/** Files removed from the index. */
|
|
15
|
+
deleted: readonly string[];
|
|
16
|
+
/** Files marked as changed but whose fingerprint matched (no-op). */
|
|
17
|
+
skipped: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Apply a delta to the on-disk index.
|
|
21
|
+
*
|
|
22
|
+
* Strategy (MVP): load the snapshot, mutate in memory, rewrite the full
|
|
23
|
+
* store. Cheap for SharkCraft-sized indexes (a few MB on disk). Per-kind
|
|
24
|
+
* append/compact is the optimisation when the cold-rewrite cost is felt.
|
|
25
|
+
*/
|
|
26
|
+
export declare function updateChanged(options: IIncrementalUpdateOptions): IIncrementalUpdateResult;
|
|
27
|
+
/**
|
|
28
|
+
* Helper: detect changed files by walking the project and comparing
|
|
29
|
+
* fingerprints against the stored snapshot. Used by
|
|
30
|
+
* `shrk graph index --changed` when no explicit file list is provided.
|
|
31
|
+
*/
|
|
32
|
+
export declare function detectChangedAndDeleted(projectRoot: string): {
|
|
33
|
+
changed: readonly string[];
|
|
34
|
+
deleted: readonly string[];
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Get the list of files changed since a git ref (e.g. `main`, `HEAD~5`,
|
|
38
|
+
* a tag). Returns project-relative POSIX paths. Errors → empty list.
|
|
39
|
+
*/
|
|
40
|
+
export declare function changedFilesSince(projectRoot: string, ref: string): readonly string[];
|
|
41
|
+
//# sourceMappingURL=incremental-updater.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"incremental-updater.d.ts","sourceRoot":"","sources":["../../src/indexer/incremental-updater.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAyC5D,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,2CAA2C;IAC3C,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,cAAc,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,oCAAoC;IACpC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,qEAAqE;IACrE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,yBAAyB,GACjC,wBAAwB,CAwM1B;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG;IAC5D,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B,CAqEA;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAcrF"}
|