@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,289 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import * as nodePath from 'node:path';
|
|
4
|
+
import { EdgeKind } from "../schema/edge-kind.js";
|
|
5
|
+
import { NodeKind } from "../schema/node-kind.js";
|
|
6
|
+
import { fingerprintFile } from "../store/file-fingerprint.js";
|
|
7
|
+
import { GraphStore } from "../store/graph-store.js";
|
|
8
|
+
import { summarizeCycles } from "../query/cycle-detection.js";
|
|
9
|
+
import { summarizeUnresolvedImports } from "./unresolved-imports.js";
|
|
10
|
+
import { detectWorkspacePackages, } from "./detect-workspace.js";
|
|
11
|
+
import { EXTRACT_TS_FILE_SOURCE, extractTsFile, stitchPerFileReferences, } from "./extract-ts-file.js";
|
|
12
|
+
import { extractPythonFile } from "./extract-python-file.js";
|
|
13
|
+
import { extractGoFile } from "./extract-go-file.js";
|
|
14
|
+
import { extractJavaFile } from "./extract-java-file.js";
|
|
15
|
+
import { extractRustFile } from "./extract-rust-file.js";
|
|
16
|
+
import { extractKotlinFile } from "./extract-kotlin-file.js";
|
|
17
|
+
import { extractRubyFile } from "./extract-ruby-file.js";
|
|
18
|
+
import { extractCsharpFile } from "./extract-csharp-file.js";
|
|
19
|
+
import { extractElixirFile } from "./extract-elixir-file.js";
|
|
20
|
+
import { extractPhpFile } from "./extract-php-file.js";
|
|
21
|
+
import { extractDartFile } from "./extract-dart-file.js";
|
|
22
|
+
import { extractSwiftFile } from "./extract-swift-file.js";
|
|
23
|
+
import { createImportResolverContext, ImportResolution, resolveImport, } from "./resolve-imports.js";
|
|
24
|
+
const SOURCE_EXTS = new Set([
|
|
25
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts',
|
|
26
|
+
// Web component formats — parsed by framework-scanners; the TS-AST
|
|
27
|
+
// extractor short-circuits on these.
|
|
28
|
+
'.vue', '.svelte', '.astro',
|
|
29
|
+
// Non-TS languages — handled by the per-language dispatcher.
|
|
30
|
+
'.py', '.go', '.java', '.rs', '.kt', '.kts', '.rb', '.cs', '.csx', '.ex', '.exs', '.php',
|
|
31
|
+
'.dart', '.swift',
|
|
32
|
+
// Schema-definition formats — File nodes only; framework-scanners
|
|
33
|
+
// does the SDL parsing.
|
|
34
|
+
'.graphql', '.gql',
|
|
35
|
+
]);
|
|
36
|
+
const SKIP_DIRS = new Set([
|
|
37
|
+
'node_modules',
|
|
38
|
+
'dist',
|
|
39
|
+
'build',
|
|
40
|
+
'coverage',
|
|
41
|
+
'.git',
|
|
42
|
+
'.sharkcraft',
|
|
43
|
+
'.next',
|
|
44
|
+
'.cache',
|
|
45
|
+
'.tmp-pack',
|
|
46
|
+
'out',
|
|
47
|
+
'target',
|
|
48
|
+
]);
|
|
49
|
+
/**
|
|
50
|
+
* Build a full graph index and write it to disk. Overwrites any
|
|
51
|
+
* pre-existing store under `<root>/.sharkcraft/graph/`.
|
|
52
|
+
*
|
|
53
|
+
* Single-process, no worker pool yet. The compiler-API per-file extractor
|
|
54
|
+
* is fast enough at SharkCraft's size; worker-pool parallelism is a
|
|
55
|
+
* later optimisation tied to measured budgets (see code-intelligence.md
|
|
56
|
+
* §7).
|
|
57
|
+
*/
|
|
58
|
+
export function buildFullIndex(options) {
|
|
59
|
+
const start = Date.now();
|
|
60
|
+
const { projectRoot } = options;
|
|
61
|
+
const ignore = new Set([...SKIP_DIRS, ...(options.extraIgnore ?? [])]);
|
|
62
|
+
const sourceFiles = walkSources(projectRoot, ignore, options.maxFiles ?? 0);
|
|
63
|
+
const workspaces = detectWorkspacePackages(projectRoot);
|
|
64
|
+
const resolverCtx = createImportResolverContext(projectRoot, workspaces);
|
|
65
|
+
const nodes = [];
|
|
66
|
+
const edges = [];
|
|
67
|
+
const fingerprints = [];
|
|
68
|
+
const packageNodes = buildPackageNodes(workspaces);
|
|
69
|
+
for (const n of packageNodes)
|
|
70
|
+
nodes.push(n);
|
|
71
|
+
const fileIdByPath = new Map();
|
|
72
|
+
const packageDirIndex = buildPackageDirIndex(workspaces);
|
|
73
|
+
// Track per-file extracted + resolved imports so the stitcher pass
|
|
74
|
+
// can emit references-symbol / calls-symbol edges with cross-file
|
|
75
|
+
// targets resolved.
|
|
76
|
+
const extractedByPath = new Map();
|
|
77
|
+
const resolvedSpecByPath = new Map();
|
|
78
|
+
const defaultExportNameByPath = new Map();
|
|
79
|
+
for (const abs of sourceFiles) {
|
|
80
|
+
const fp = fingerprintFile(abs, projectRoot);
|
|
81
|
+
fingerprints.push(fp);
|
|
82
|
+
fileIdByPath.set(fp.path, fp.nodeId);
|
|
83
|
+
const extracted = fp.language === 'python' ? extractPythonFile(fp, abs)
|
|
84
|
+
: fp.language === 'go' ? extractGoFile(fp, abs)
|
|
85
|
+
: fp.language === 'java' ? extractJavaFile(fp, abs)
|
|
86
|
+
: fp.language === 'rust' ? extractRustFile(fp, abs)
|
|
87
|
+
: fp.language === 'kotlin' ? extractKotlinFile(fp, abs)
|
|
88
|
+
: fp.language === 'ruby' ? extractRubyFile(fp, abs)
|
|
89
|
+
: fp.language === 'csharp' ? extractCsharpFile(fp, abs)
|
|
90
|
+
: fp.language === 'elixir' ? extractElixirFile(fp, abs)
|
|
91
|
+
: fp.language === 'php' ? extractPhpFile(fp, abs)
|
|
92
|
+
: fp.language === 'dart' ? extractDartFile(fp, abs)
|
|
93
|
+
: fp.language === 'swift' ? extractSwiftFile(fp, abs)
|
|
94
|
+
: extractTsFile(fp, abs);
|
|
95
|
+
nodes.push(extracted.fileNode);
|
|
96
|
+
for (const sym of extracted.symbolNodes)
|
|
97
|
+
nodes.push(sym);
|
|
98
|
+
for (const e of extracted.edges)
|
|
99
|
+
edges.push(e);
|
|
100
|
+
extractedByPath.set(fp.path, extracted);
|
|
101
|
+
defaultExportNameByPath.set(fp.path, extracted.fileNode.data?.['defaultExportName'] ?? undefined);
|
|
102
|
+
// BelongsToPackage edge for files inside a known package dir.
|
|
103
|
+
const pkg = findOwningPackage(fp.path, packageDirIndex);
|
|
104
|
+
if (pkg) {
|
|
105
|
+
edges.push(buildEdge(fp.nodeId, `package:${pkg.name}`, EdgeKind.BelongsToPackage, EXTRACT_TS_FILE_SOURCE));
|
|
106
|
+
}
|
|
107
|
+
// ImportsFile edges (resolved where possible).
|
|
108
|
+
const resolvedSpec = new Map();
|
|
109
|
+
for (const raw of extracted.rawImportSpecifiers) {
|
|
110
|
+
const r = resolveImport(raw.specifier, abs, resolverCtx);
|
|
111
|
+
resolvedSpec.set(raw.specifier, r.targetPath);
|
|
112
|
+
const data = {
|
|
113
|
+
specifier: r.specifier,
|
|
114
|
+
line: raw.line,
|
|
115
|
+
importKind: raw.kind,
|
|
116
|
+
resolutionKind: r.kind,
|
|
117
|
+
};
|
|
118
|
+
if (r.targetPath) {
|
|
119
|
+
const targetId = `file:${r.targetPath}`;
|
|
120
|
+
edges.push(buildEdge(fp.nodeId, targetId, EdgeKind.ImportsFile, EXTRACT_TS_FILE_SOURCE, data));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const externalId = r.kind === ImportResolution.External
|
|
124
|
+
? `external:${r.specifier}`
|
|
125
|
+
: `unresolved:${r.specifier}`;
|
|
126
|
+
edges.push(buildEdge(fp.nodeId, externalId, EdgeKind.ImportsFile, EXTRACT_TS_FILE_SOURCE, data));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
resolvedSpecByPath.set(fp.path, resolvedSpec);
|
|
130
|
+
}
|
|
131
|
+
// Stitch references / calls edges now that bindings + targets are all
|
|
132
|
+
// collected. Loops over the same files; cheap.
|
|
133
|
+
for (const [path, extracted] of extractedByPath) {
|
|
134
|
+
const localNames = new Map();
|
|
135
|
+
for (const sym of extracted.symbolNodes)
|
|
136
|
+
localNames.set(sym.label, sym.id);
|
|
137
|
+
const refEdges = stitchPerFileReferences({
|
|
138
|
+
fileNodeId: extracted.fileNode.id,
|
|
139
|
+
extracted,
|
|
140
|
+
resolvedSpec: resolvedSpecByPath.get(path) ?? new Map(),
|
|
141
|
+
defaultExportNameByPath,
|
|
142
|
+
localSymbolNamesInThisFile: localNames,
|
|
143
|
+
});
|
|
144
|
+
for (const e of refEdges)
|
|
145
|
+
edges.push(e);
|
|
146
|
+
}
|
|
147
|
+
// PackageDependsOn aggregates: collapse internal ImportsFile edges to
|
|
148
|
+
// their owning package on both sides.
|
|
149
|
+
collectPackageDependsOn(edges, packageDirIndex);
|
|
150
|
+
// Drop duplicate edges (extractor may emit identical edges for `export
|
|
151
|
+
// { foo } from './foo'` and an `import` re-using the same line — same
|
|
152
|
+
// hashed id, last write wins). Edge dedupe by id.
|
|
153
|
+
const seen = new Set();
|
|
154
|
+
const dedupedEdges = [];
|
|
155
|
+
for (const e of edges) {
|
|
156
|
+
if (seen.has(e.id))
|
|
157
|
+
continue;
|
|
158
|
+
seen.add(e.id);
|
|
159
|
+
dedupedEdges.push(e);
|
|
160
|
+
}
|
|
161
|
+
const store = new GraphStore(projectRoot);
|
|
162
|
+
const cycles = summarizeCycles(nodes, dedupedEdges);
|
|
163
|
+
const unresolved = summarizeUnresolvedImports(dedupedEdges);
|
|
164
|
+
const manifest = store.writeSnapshot(nodes, dedupedEdges, fingerprints, {
|
|
165
|
+
projectRoot,
|
|
166
|
+
lastIndexedAt: new Date().toISOString(),
|
|
167
|
+
lastIndexDurationMs: Date.now() - start,
|
|
168
|
+
filesIndexed: fingerprints.length,
|
|
169
|
+
workspacePackages: workspaces.map((w) => w.name),
|
|
170
|
+
// nodesByKind / edgesByKind are filled in by the store.
|
|
171
|
+
nodesByKind: {},
|
|
172
|
+
edgesByKind: {},
|
|
173
|
+
cycleCount: cycles.cycleCount,
|
|
174
|
+
largestCycleSize: cycles.largestCycleSize,
|
|
175
|
+
filesInCycles: cycles.filesInCycles,
|
|
176
|
+
unresolvedImportCount: unresolved.unresolvedImportCount,
|
|
177
|
+
filesWithUnresolvedImports: unresolved.filesWithUnresolvedImports,
|
|
178
|
+
unresolvedImportSamples: unresolved.unresolvedImportSamples,
|
|
179
|
+
});
|
|
180
|
+
return { manifest, durationMs: Date.now() - start };
|
|
181
|
+
}
|
|
182
|
+
function walkSources(projectRoot, ignore, maxFiles) {
|
|
183
|
+
const out = [];
|
|
184
|
+
const stack = [projectRoot];
|
|
185
|
+
while (stack.length > 0) {
|
|
186
|
+
if (maxFiles > 0 && out.length >= maxFiles)
|
|
187
|
+
break;
|
|
188
|
+
const dir = stack.pop();
|
|
189
|
+
let entries;
|
|
190
|
+
try {
|
|
191
|
+
entries = readdirSync(dir);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
for (const name of entries) {
|
|
197
|
+
if (ignore.has(name))
|
|
198
|
+
continue;
|
|
199
|
+
if (name.startsWith('.') && name !== '.')
|
|
200
|
+
continue;
|
|
201
|
+
const full = nodePath.join(dir, name);
|
|
202
|
+
let st;
|
|
203
|
+
try {
|
|
204
|
+
st = statSync(full);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (st.isDirectory()) {
|
|
210
|
+
stack.push(full);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (!st.isFile())
|
|
214
|
+
continue;
|
|
215
|
+
if (!SOURCE_EXTS.has(nodePath.extname(full).toLowerCase()))
|
|
216
|
+
continue;
|
|
217
|
+
out.push(full);
|
|
218
|
+
if (maxFiles > 0 && out.length >= maxFiles)
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return out.sort();
|
|
223
|
+
}
|
|
224
|
+
function buildPackageNodes(packages) {
|
|
225
|
+
return packages.map((p) => ({
|
|
226
|
+
id: `package:${p.name}`,
|
|
227
|
+
kind: NodeKind.Package,
|
|
228
|
+
label: p.name,
|
|
229
|
+
path: p.dir,
|
|
230
|
+
data: {
|
|
231
|
+
...(p.entry ? { entry: p.entry } : {}),
|
|
232
|
+
},
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
function buildPackageDirIndex(packages) {
|
|
236
|
+
// Sorted by length desc so the most-specific match wins (e.g.
|
|
237
|
+
// `packages/foo/sub` resolves before `packages/foo`).
|
|
238
|
+
const entries = [...packages]
|
|
239
|
+
.map((p) => ({ dir: p.dir.replace(/\/+$/, ''), name: p.name }))
|
|
240
|
+
.sort((a, b) => b.dir.length - a.dir.length);
|
|
241
|
+
return { entries };
|
|
242
|
+
}
|
|
243
|
+
function findOwningPackage(filePath, index) {
|
|
244
|
+
for (const e of index.entries) {
|
|
245
|
+
if (filePath === e.dir || filePath.startsWith(e.dir + '/')) {
|
|
246
|
+
return e;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
function collectPackageDependsOn(edges, index) {
|
|
252
|
+
const pairs = new Map();
|
|
253
|
+
for (const e of edges) {
|
|
254
|
+
if (e.kind !== EdgeKind.ImportsFile)
|
|
255
|
+
continue;
|
|
256
|
+
const fromFile = stripPrefix(e.from, 'file:');
|
|
257
|
+
const toFile = stripPrefix(e.to, 'file:');
|
|
258
|
+
if (!fromFile || !toFile)
|
|
259
|
+
continue; // skip external / unresolved targets
|
|
260
|
+
const fromPkg = findOwningPackage(fromFile, index);
|
|
261
|
+
const toPkg = findOwningPackage(toFile, index);
|
|
262
|
+
if (!fromPkg || !toPkg)
|
|
263
|
+
continue;
|
|
264
|
+
if (fromPkg.name === toPkg.name)
|
|
265
|
+
continue;
|
|
266
|
+
const k = `${fromPkg.name}|${toPkg.name}`;
|
|
267
|
+
const cur = pairs.get(k);
|
|
268
|
+
if (cur)
|
|
269
|
+
cur.count += 1;
|
|
270
|
+
else
|
|
271
|
+
pairs.set(k, { from: fromPkg.name, to: toPkg.name, count: 1 });
|
|
272
|
+
}
|
|
273
|
+
for (const { from, to, count } of pairs.values()) {
|
|
274
|
+
edges.push(buildEdge(`package:${from}`, `package:${to}`, EdgeKind.PackageDependsOn, 'index-builder@v1', { count }));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function buildEdge(from, to, kind, source, data) {
|
|
278
|
+
return {
|
|
279
|
+
id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
|
|
280
|
+
from,
|
|
281
|
+
to,
|
|
282
|
+
kind,
|
|
283
|
+
source,
|
|
284
|
+
...(data ? { data } : {}),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function stripPrefix(id, prefix) {
|
|
288
|
+
return id.startsWith(prefix) ? id.slice(prefix.length) : undefined;
|
|
289
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ITsconfigPathsMap } from '@shrkcrft/boundaries';
|
|
2
|
+
import type { IWorkspacePackage } from './detect-workspace.js';
|
|
3
|
+
export declare enum ImportResolution {
|
|
4
|
+
Relative = "relative",
|
|
5
|
+
Alias = "alias",
|
|
6
|
+
Workspace = "workspace",
|
|
7
|
+
External = "external",
|
|
8
|
+
Unresolved = "unresolved"
|
|
9
|
+
}
|
|
10
|
+
export interface IResolvedImport {
|
|
11
|
+
/** Project-relative path to the target file, POSIX, when resolved. */
|
|
12
|
+
targetPath?: string;
|
|
13
|
+
kind: ImportResolution;
|
|
14
|
+
/** Specifier as it appeared in source. */
|
|
15
|
+
specifier: string;
|
|
16
|
+
}
|
|
17
|
+
export interface IImportResolverContext {
|
|
18
|
+
projectRoot: string;
|
|
19
|
+
workspacePackages: readonly IWorkspacePackage[];
|
|
20
|
+
tsconfigPaths: ITsconfigPathsMap;
|
|
21
|
+
}
|
|
22
|
+
export declare function createImportResolverContext(projectRoot: string, workspacePackages: readonly IWorkspacePackage[]): IImportResolverContext;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve an import specifier to a project-relative file path.
|
|
25
|
+
*
|
|
26
|
+
* Resolution order (cheapest first):
|
|
27
|
+
* 1. Relative (`./` or `../`) against the source file's directory.
|
|
28
|
+
* 2. tsconfig path aliases.
|
|
29
|
+
* 3. Workspace package name (exact match or `<pkg>/<sub>`).
|
|
30
|
+
* 4. External — left unresolved with `ImportResolution.External`.
|
|
31
|
+
*
|
|
32
|
+
* `fromAbsPath` is the absolute path of the importing file; needed for
|
|
33
|
+
* relative resolution.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveImport(specifier: string, fromAbsPath: string, ctx: IImportResolverContext): IResolvedImport;
|
|
36
|
+
//# sourceMappingURL=resolve-imports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-imports.d.ts","sourceRoot":"","sources":["../../src/indexer/resolve-imports.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAI/D,oBAAY,gBAAgB;IAC1B,QAAQ,aAAa;IACrB,KAAK,UAAU;IACf,SAAS,cAAc;IACvB,QAAQ,aAAa;IACrB,UAAU,eAAe;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAChD,aAAa,EAAE,iBAAiB,CAAC;CAClC;AAED,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,SAAS,iBAAiB,EAAE,GAC9C,sBAAsB,CAMxB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,sBAAsB,GAC1B,eAAe,CAwCjB"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
3
|
+
import { loadTsconfigPaths, resolveAliasCandidates, } from '@shrkcrft/boundaries';
|
|
4
|
+
const PROBE_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts'];
|
|
5
|
+
export var ImportResolution;
|
|
6
|
+
(function (ImportResolution) {
|
|
7
|
+
ImportResolution["Relative"] = "relative";
|
|
8
|
+
ImportResolution["Alias"] = "alias";
|
|
9
|
+
ImportResolution["Workspace"] = "workspace";
|
|
10
|
+
ImportResolution["External"] = "external";
|
|
11
|
+
ImportResolution["Unresolved"] = "unresolved";
|
|
12
|
+
})(ImportResolution || (ImportResolution = {}));
|
|
13
|
+
export function createImportResolverContext(projectRoot, workspacePackages) {
|
|
14
|
+
return {
|
|
15
|
+
projectRoot,
|
|
16
|
+
workspacePackages,
|
|
17
|
+
tsconfigPaths: loadTsconfigPaths(projectRoot),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve an import specifier to a project-relative file path.
|
|
22
|
+
*
|
|
23
|
+
* Resolution order (cheapest first):
|
|
24
|
+
* 1. Relative (`./` or `../`) against the source file's directory.
|
|
25
|
+
* 2. tsconfig path aliases.
|
|
26
|
+
* 3. Workspace package name (exact match or `<pkg>/<sub>`).
|
|
27
|
+
* 4. External — left unresolved with `ImportResolution.External`.
|
|
28
|
+
*
|
|
29
|
+
* `fromAbsPath` is the absolute path of the importing file; needed for
|
|
30
|
+
* relative resolution.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveImport(specifier, fromAbsPath, ctx) {
|
|
33
|
+
if (specifier.startsWith('.')) {
|
|
34
|
+
const dir = nodePath.dirname(fromAbsPath);
|
|
35
|
+
const probe = probeCandidate(nodePath.resolve(dir, specifier));
|
|
36
|
+
if (probe) {
|
|
37
|
+
return {
|
|
38
|
+
specifier,
|
|
39
|
+
targetPath: toProjectRel(ctx.projectRoot, probe),
|
|
40
|
+
kind: ImportResolution.Relative,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return { specifier, kind: ImportResolution.Unresolved };
|
|
44
|
+
}
|
|
45
|
+
const aliasCandidates = resolveAliasCandidates(specifier, ctx.tsconfigPaths);
|
|
46
|
+
for (const cand of aliasCandidates) {
|
|
47
|
+
const abs = nodePath.resolve(ctx.projectRoot, cand);
|
|
48
|
+
const probe = probeCandidate(abs);
|
|
49
|
+
if (probe) {
|
|
50
|
+
return {
|
|
51
|
+
specifier,
|
|
52
|
+
targetPath: toProjectRel(ctx.projectRoot, probe),
|
|
53
|
+
kind: ImportResolution.Alias,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const ws = findWorkspacePackage(specifier, ctx.workspacePackages);
|
|
58
|
+
if (ws) {
|
|
59
|
+
const probe = resolveWorkspaceTarget(specifier, ws, ctx.projectRoot);
|
|
60
|
+
if (probe) {
|
|
61
|
+
return {
|
|
62
|
+
specifier,
|
|
63
|
+
targetPath: toProjectRel(ctx.projectRoot, probe),
|
|
64
|
+
kind: ImportResolution.Workspace,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { specifier, kind: ImportResolution.External };
|
|
69
|
+
}
|
|
70
|
+
function probeCandidate(absPathNoExt) {
|
|
71
|
+
// If the path already has a known extension and exists, return it.
|
|
72
|
+
const ext = nodePath.extname(absPathNoExt);
|
|
73
|
+
if (PROBE_EXTS.includes(ext) && existsSafe(absPathNoExt) && isFile(absPathNoExt)) {
|
|
74
|
+
return absPathNoExt;
|
|
75
|
+
}
|
|
76
|
+
// Try appending each known extension.
|
|
77
|
+
for (const e of PROBE_EXTS) {
|
|
78
|
+
const cand = absPathNoExt + e;
|
|
79
|
+
if (existsSafe(cand) && isFile(cand))
|
|
80
|
+
return cand;
|
|
81
|
+
}
|
|
82
|
+
// Try as a directory with index.<ext>.
|
|
83
|
+
if (existsSafe(absPathNoExt) && isDir(absPathNoExt)) {
|
|
84
|
+
for (const e of PROBE_EXTS) {
|
|
85
|
+
const cand = nodePath.join(absPathNoExt, `index${e}`);
|
|
86
|
+
if (existsSafe(cand) && isFile(cand))
|
|
87
|
+
return cand;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
function findWorkspacePackage(specifier, packages) {
|
|
93
|
+
for (const p of packages) {
|
|
94
|
+
if (specifier === p.name)
|
|
95
|
+
return p;
|
|
96
|
+
if (specifier.startsWith(p.name + '/'))
|
|
97
|
+
return p;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
function resolveWorkspaceTarget(specifier, pkg, projectRoot) {
|
|
102
|
+
// Exact match → resolve via package entry, falling back to src/index.<ext>.
|
|
103
|
+
if (specifier === pkg.name) {
|
|
104
|
+
if (pkg.entry) {
|
|
105
|
+
const cand = nodePath.resolve(projectRoot, pkg.entry);
|
|
106
|
+
if (existsSafe(cand) && isFile(cand))
|
|
107
|
+
return cand;
|
|
108
|
+
// Some packages list ./dist/index.js as main; for source-time graphs
|
|
109
|
+
// we'd rather hit the src/. Try that next.
|
|
110
|
+
}
|
|
111
|
+
const srcIndex = nodePath.resolve(projectRoot, pkg.dir, 'src');
|
|
112
|
+
return probeCandidate(srcIndex);
|
|
113
|
+
}
|
|
114
|
+
// Subpath: `<pkg>/foo/bar`.
|
|
115
|
+
const sub = specifier.slice(pkg.name.length + 1);
|
|
116
|
+
return probeCandidate(nodePath.resolve(projectRoot, pkg.dir, sub));
|
|
117
|
+
}
|
|
118
|
+
function existsSafe(p) {
|
|
119
|
+
try {
|
|
120
|
+
return existsSync(p);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function isFile(p) {
|
|
127
|
+
try {
|
|
128
|
+
return statSync(p).isFile();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function isDir(p) {
|
|
135
|
+
try {
|
|
136
|
+
return statSync(p).isDirectory();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function toProjectRel(projectRoot, abs) {
|
|
143
|
+
return nodePath.relative(projectRoot, abs).split(nodePath.sep).join('/');
|
|
144
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IEdge } from '../schema/edge.js';
|
|
2
|
+
export interface IUnresolvedImportSummary {
|
|
3
|
+
/** Total `unresolved:<spec>` ImportsFile edges. */
|
|
4
|
+
unresolvedImportCount: number;
|
|
5
|
+
/** Distinct file ids with at least one unresolved import. */
|
|
6
|
+
filesWithUnresolvedImports: number;
|
|
7
|
+
/** Up to `sampleLimit` distinct specifier strings. */
|
|
8
|
+
unresolvedImportSamples: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Roll up unresolved imports from the indexer's edge list. The indexer
|
|
12
|
+
* already emits edges targeting `unresolved:<spec>` when the resolver
|
|
13
|
+
* fails (relative path doesn't exist, alias points nowhere, workspace
|
|
14
|
+
* package not found). This helper counts them so the manifest can
|
|
15
|
+
* carry a stable counter the doctor + dashboard read from.
|
|
16
|
+
*
|
|
17
|
+
* Pure function over the dedupe'd edge set; no I/O.
|
|
18
|
+
*/
|
|
19
|
+
export declare function summarizeUnresolvedImports(edges: readonly IEdge[], sampleLimit?: number): IUnresolvedImportSummary;
|
|
20
|
+
//# sourceMappingURL=unresolved-imports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unresolved-imports.d.ts","sourceRoot":"","sources":["../../src/indexer/unresolved-imports.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,MAAM,WAAW,wBAAwB;IACvC,mDAAmD;IACnD,qBAAqB,EAAE,MAAM,CAAC;IAC9B,6DAA6D;IAC7D,0BAA0B,EAAE,MAAM,CAAC;IACnC,sDAAsD;IACtD,uBAAuB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5C;AAID;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,WAAW,GAAE,MAA6B,GACzC,wBAAwB,CAkB1B"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { EdgeKind } from "../schema/edge-kind.js";
|
|
2
|
+
const DEFAULT_SAMPLE_LIMIT = 10;
|
|
3
|
+
/**
|
|
4
|
+
* Roll up unresolved imports from the indexer's edge list. The indexer
|
|
5
|
+
* already emits edges targeting `unresolved:<spec>` when the resolver
|
|
6
|
+
* fails (relative path doesn't exist, alias points nowhere, workspace
|
|
7
|
+
* package not found). This helper counts them so the manifest can
|
|
8
|
+
* carry a stable counter the doctor + dashboard read from.
|
|
9
|
+
*
|
|
10
|
+
* Pure function over the dedupe'd edge set; no I/O.
|
|
11
|
+
*/
|
|
12
|
+
export function summarizeUnresolvedImports(edges, sampleLimit = DEFAULT_SAMPLE_LIMIT) {
|
|
13
|
+
let unresolvedImportCount = 0;
|
|
14
|
+
const fileSet = new Set();
|
|
15
|
+
const sampleSet = new Set();
|
|
16
|
+
for (const e of edges) {
|
|
17
|
+
if (e.kind !== EdgeKind.ImportsFile)
|
|
18
|
+
continue;
|
|
19
|
+
if (!e.to.startsWith('unresolved:'))
|
|
20
|
+
continue;
|
|
21
|
+
unresolvedImportCount += 1;
|
|
22
|
+
fileSet.add(e.from);
|
|
23
|
+
if (sampleSet.size < sampleLimit) {
|
|
24
|
+
sampleSet.add(e.to.slice('unresolved:'.length));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
unresolvedImportCount,
|
|
29
|
+
filesWithUnresolvedImports: fileSet.size,
|
|
30
|
+
unresolvedImportSamples: [...sampleSet].sort(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { IEdge } from '../schema/edge.js';
|
|
2
|
+
import type { INode } from '../schema/node.js';
|
|
3
|
+
export interface ICycleSummary {
|
|
4
|
+
/** Number of strongly-connected components of size ≥ 2. */
|
|
5
|
+
cycleCount: number;
|
|
6
|
+
/** Size of the largest SCC of size ≥ 2 (0 if no cycles). */
|
|
7
|
+
largestCycleSize: number;
|
|
8
|
+
/** Total number of file nodes participating in some cycle. */
|
|
9
|
+
filesInCycles: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A single import cycle. `nodeIds` are the participating `file:<path>`
|
|
13
|
+
* node ids, in iteration order from Tarjan SCC (the entry node is at
|
|
14
|
+
* index 0; the cycle is undirected from a presentation standpoint).
|
|
15
|
+
* Renderers usually want to display `paths` instead — file ids are
|
|
16
|
+
* stable but bear the `file:` prefix.
|
|
17
|
+
*/
|
|
18
|
+
export interface IFileCycle {
|
|
19
|
+
/** Stable file node ids that form the cycle. */
|
|
20
|
+
nodeIds: readonly string[];
|
|
21
|
+
/** Project-relative file paths (filled when callers can resolve them). */
|
|
22
|
+
paths?: readonly string[];
|
|
23
|
+
/** Cycle size (== nodeIds.length). */
|
|
24
|
+
size: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Find every import cycle in the file-import directed graph. Returns
|
|
28
|
+
* one entry per SCC of size ≥ 2. Iterative Tarjan SCC over the
|
|
29
|
+
* `imports-file` subgraph; O(V+E) and stack-safe.
|
|
30
|
+
*
|
|
31
|
+
* `pathById` is optional — when supplied, the returned `paths` array
|
|
32
|
+
* is populated so callers don't have to re-resolve file ids.
|
|
33
|
+
*/
|
|
34
|
+
export declare function findFileCycles(nodes: readonly INode[], edges: readonly IEdge[], pathById?: ReadonlyMap<string, string>): readonly IFileCycle[];
|
|
35
|
+
/**
|
|
36
|
+
* Roll-up over `findFileCycles` results. Kept for downstream callers
|
|
37
|
+
* (the indexer, doctor) that only care about counts.
|
|
38
|
+
*/
|
|
39
|
+
export declare function summarizeCycles(nodes: readonly INode[], edges: readonly IEdge[]): ICycleSummary;
|
|
40
|
+
//# sourceMappingURL=cycle-detection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cycle-detection.d.ts","sourceRoot":"","sources":["../../src/query/cycle-detection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAG/C,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,gDAAgD;IAChD,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,0EAA0E;IAC1E,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACrC,SAAS,UAAU,EAAE,CAsBvB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,KAAK,EAAE,SAAS,KAAK,EAAE,GACtB,aAAa,CAaf"}
|