@crouton-kit/tsym 0.1.0
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/LICENSE +21 -0
- package/README.md +51 -0
- package/bin/tsym.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +89 -0
- package/dist/commands/handlers/check.d.ts +3 -0
- package/dist/commands/handlers/check.js +94 -0
- package/dist/commands/handlers/find.d.ts +3 -0
- package/dist/commands/handlers/find.js +56 -0
- package/dist/commands/handlers/inspect.d.ts +3 -0
- package/dist/commands/handlers/inspect.js +81 -0
- package/dist/commands/handlers/outline.d.ts +3 -0
- package/dist/commands/handlers/outline.js +101 -0
- package/dist/commands/handlers/query.d.ts +3 -0
- package/dist/commands/handlers/query.js +196 -0
- package/dist/commands/handlers/rename.d.ts +3 -0
- package/dist/commands/handlers/rename.js +53 -0
- package/dist/commands/handlers/search.d.ts +5 -0
- package/dist/commands/handlers/search.js +32 -0
- package/dist/commands/handlers/server-reload.d.ts +3 -0
- package/dist/commands/handlers/server-reload.js +7 -0
- package/dist/commands/handlers/server-status.d.ts +3 -0
- package/dist/commands/handlers/server-status.js +7 -0
- package/dist/commands/handlers/server-stop.d.ts +3 -0
- package/dist/commands/handlers/server-stop.js +7 -0
- package/dist/commands/handlers/show.d.ts +3 -0
- package/dist/commands/handlers/show.js +63 -0
- package/dist/commands/handlers/type.d.ts +3 -0
- package/dist/commands/handlers/type.js +45 -0
- package/dist/commands/handlers.d.ts +12 -0
- package/dist/commands/handlers.js +44 -0
- package/dist/commands/help.d.ts +2 -0
- package/dist/commands/help.js +27 -0
- package/dist/commands/rename-apply.d.ts +37 -0
- package/dist/commands/rename-apply.js +115 -0
- package/dist/commands/tree.d.ts +9 -0
- package/dist/commands/tree.js +277 -0
- package/dist/commands/types.d.ts +80 -0
- package/dist/commands/types.js +1 -0
- package/dist/core/declarations.d.ts +48 -0
- package/dist/core/declarations.js +153 -0
- package/dist/core/receipt.d.ts +7 -0
- package/dist/core/receipt.js +9 -0
- package/dist/core/search.d.ts +29 -0
- package/dist/core/search.js +47 -0
- package/dist/core/source.d.ts +32 -0
- package/dist/core/source.js +96 -0
- package/dist/core/workspace.d.ts +28 -0
- package/dist/core/workspace.js +137 -0
- package/dist/index/build.d.ts +22 -0
- package/dist/index/build.js +79 -0
- package/dist/index/classify.d.ts +20 -0
- package/dist/index/classify.js +60 -0
- package/dist/index/commits.d.ts +15 -0
- package/dist/index/commits.js +201 -0
- package/dist/index/descriptor.d.ts +19 -0
- package/dist/index/descriptor.js +295 -0
- package/dist/index/graph.d.ts +82 -0
- package/dist/index/graph.js +1 -0
- package/dist/index/heritage.d.ts +15 -0
- package/dist/index/heritage.js +122 -0
- package/dist/index/refresh.d.ts +28 -0
- package/dist/index/refresh.js +110 -0
- package/dist/index/scope.d.ts +4 -0
- package/dist/index/scope.js +21 -0
- package/dist/index/state.d.ts +22 -0
- package/dist/index/state.js +63 -0
- package/dist/index/walk.d.ts +23 -0
- package/dist/index/walk.js +550 -0
- package/dist/index/watch.d.ts +18 -0
- package/dist/index/watch.js +175 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.js +54 -0
- package/dist/read/read.d.ts +32 -0
- package/dist/read/read.js +89 -0
- package/dist/relations/graph.d.ts +23 -0
- package/dist/relations/graph.js +115 -0
- package/dist/relations/mermaid.d.ts +3 -0
- package/dist/relations/mermaid.js +37 -0
- package/dist/relations/store-relations.d.ts +20 -0
- package/dist/relations/store-relations.js +158 -0
- package/dist/relations/types.d.ts +58 -0
- package/dist/relations/types.js +12 -0
- package/dist/server/client.d.ts +5 -0
- package/dist/server/client.js +215 -0
- package/dist/server/context.d.ts +21 -0
- package/dist/server/context.js +12 -0
- package/dist/server/lifecycle.d.ts +28 -0
- package/dist/server/lifecycle.js +62 -0
- package/dist/server/main.d.ts +1 -0
- package/dist/server/main.js +541 -0
- package/dist/server/projects.d.ts +21 -0
- package/dist/server/projects.js +28 -0
- package/dist/server/wire.d.ts +39 -0
- package/dist/server/wire.js +19 -0
- package/dist/store/load.d.ts +67 -0
- package/dist/store/load.js +193 -0
- package/dist/store/meta.d.ts +28 -0
- package/dist/store/meta.js +137 -0
- package/dist/store/query.d.ts +5 -0
- package/dist/store/query.js +4 -0
- package/dist/store/schema.d.ts +4 -0
- package/dist/store/schema.js +19 -0
- package/dist/store/store.d.ts +36 -0
- package/dist/store/store.js +240 -0
- package/dist/ts7/api.d.ts +35 -0
- package/dist/ts7/api.js +164 -0
- package/dist/ts7/ast.d.ts +1 -0
- package/dist/ts7/ast.js +1 -0
- package/dist/ts7/checker.d.ts +55 -0
- package/dist/ts7/checker.js +383 -0
- package/dist/ts7/projects.d.ts +27 -0
- package/dist/ts7/projects.js +94 -0
- package/package.json +47 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { query } from '../store/query.js';
|
|
4
|
+
export class DeclarationError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
candidates;
|
|
7
|
+
constructor(code, message, candidates = []) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.candidates = candidates;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class DeclarationResolutionErrors extends Error {
|
|
14
|
+
errors;
|
|
15
|
+
constructor(errors) {
|
|
16
|
+
super(errors.map((error) => error.message).join('\n'));
|
|
17
|
+
this.errors = errors;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Resolves every declaration identity from the committed index. */
|
|
21
|
+
export class DeclarationResolver {
|
|
22
|
+
root;
|
|
23
|
+
source;
|
|
24
|
+
storeFor;
|
|
25
|
+
constructor(root, source, storeFor) {
|
|
26
|
+
this.root = root;
|
|
27
|
+
this.source = source;
|
|
28
|
+
this.storeFor = storeFor;
|
|
29
|
+
}
|
|
30
|
+
async resolve(input) {
|
|
31
|
+
const address = parseAddress(input);
|
|
32
|
+
return address ? this.resolveAddress(address) : this.resolveName(input);
|
|
33
|
+
}
|
|
34
|
+
async resolveAll(inputs) {
|
|
35
|
+
const records = [];
|
|
36
|
+
const errors = [];
|
|
37
|
+
for (const input of inputs) {
|
|
38
|
+
try {
|
|
39
|
+
records.push(await this.resolve(input));
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof DeclarationError)
|
|
43
|
+
errors.push(error);
|
|
44
|
+
else
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (errors.length > 0)
|
|
49
|
+
throw new DeclarationResolutionErrors(errors);
|
|
50
|
+
return records;
|
|
51
|
+
}
|
|
52
|
+
async resolveAddress(address) {
|
|
53
|
+
const file = path.resolve(this.root, address.path);
|
|
54
|
+
if (!existsSync(file))
|
|
55
|
+
throw new DeclarationError('no-such-path', `${address.path} does not exist`);
|
|
56
|
+
if (address.line === 0 || address.col === 0) {
|
|
57
|
+
if (address.line !== 0 || address.col !== 0)
|
|
58
|
+
throw new DeclarationError('invalid-address', `${inputAddress(address)} mixes zero module coordinates with a source position`);
|
|
59
|
+
const module = this.rows(`MATCH (d:Declaration) WHERE d.file = $file AND d.line = 0 AND d.col = 0 RETURN ${columns()}`, { file: relative(this.root, file) });
|
|
60
|
+
if (module.length === 0)
|
|
61
|
+
throw this.notInProgram(address.path);
|
|
62
|
+
return this.record(module[0]);
|
|
63
|
+
}
|
|
64
|
+
this.validatePosition(address, file);
|
|
65
|
+
const relativeFile = relative(this.root, file);
|
|
66
|
+
const source = this.source.read(file);
|
|
67
|
+
const position = offsetAt(source, address.line, address.col);
|
|
68
|
+
const declarations = this.rows(`MATCH (d:Declaration) WHERE d.file = $file AND d.line = $line RETURN ${columns()}`, { file: relativeFile, line: address.line })
|
|
69
|
+
.filter((row) => {
|
|
70
|
+
const nameSpan = this.source.declarationNameSpanAt(file, number(row, 'line'), number(row, 'col'));
|
|
71
|
+
return nameSpan !== null && nameSpan.start <= position && position < nameSpan.end;
|
|
72
|
+
});
|
|
73
|
+
const references = declarations.length === 0
|
|
74
|
+
? this.rows(`MATCH (owner:Declaration)-[r:REFERENCES]->(d:Declaration) WHERE owner.file = $file AND r.line = $line AND r.col <= $col RETURN ${columns()}, r.col AS referenceCol`, { file: relativeFile, line: address.line, col: address.col })
|
|
75
|
+
.filter((row) => address.col < number(row, 'referenceCol') + identifierLengthAt(source, address.line, number(row, 'referenceCol')))
|
|
76
|
+
: [];
|
|
77
|
+
const enclosing = declarations.length === 0 && references.length === 0
|
|
78
|
+
? this.rows(`MATCH (d:Declaration) WHERE d.file = $file AND d.line > 0 AND d.endLine - d.lines + 1 <= $line AND $line <= d.endLine RETURN ${columns()}`, { file: relativeFile, line: address.line })
|
|
79
|
+
.flatMap((row) => {
|
|
80
|
+
const span = this.source.declarationSpanAt(file, number(row, 'line'), number(row, 'col'));
|
|
81
|
+
return span && span.start <= position && position < span.end ? [{ row, span }] : [];
|
|
82
|
+
})
|
|
83
|
+
.sort((left, right) => left.span.end - left.span.start - (right.span.end - right.span.start) || right.span.start - left.span.start)
|
|
84
|
+
.slice(0, 1)
|
|
85
|
+
.map(({ row }) => row)
|
|
86
|
+
: [];
|
|
87
|
+
const records = unique((declarations.length > 0 ? declarations : references.length > 0 ? references : enclosing).map((row) => this.record(row)));
|
|
88
|
+
if (records.length === 0) {
|
|
89
|
+
if (this.rows('MATCH (d:Declaration) WHERE d.file = $file AND d.line = 0 RETURN d.key AS key', { file: relativeFile }).length === 0)
|
|
90
|
+
throw this.notInProgram(address.path);
|
|
91
|
+
throw new DeclarationError('unresolvable-symbol', this.unresolvableAddress(address, relativeFile));
|
|
92
|
+
}
|
|
93
|
+
if (records.length > 1)
|
|
94
|
+
throw new DeclarationError('ambiguous-symbol', `${inputAddress(address)} resolves to several declarations`, records.map((item) => item.address));
|
|
95
|
+
return records[0];
|
|
96
|
+
}
|
|
97
|
+
async resolveName(name) {
|
|
98
|
+
const qualified = name.includes('.');
|
|
99
|
+
const declarations = this.rows(`MATCH (d:Declaration) WHERE d.${qualified ? 'qname' : 'name'} = $name RETURN ${columns()}`, { name })
|
|
100
|
+
.filter((row) => text(row, 'name') !== '<module>' && (qualified || text(row, 'container').endsWith('#<module>')));
|
|
101
|
+
const aliases = qualified ? [] : this.rows(`MATCH (:Declaration)-[e:EXPORTS]->(d:Declaration) WHERE e.name = $name RETURN ${columns()}`, { name })
|
|
102
|
+
.filter((row) => text(row, 'container').endsWith('#<module>'));
|
|
103
|
+
const normalized = unique([...declarations, ...aliases].map((row) => this.record(row)));
|
|
104
|
+
if (normalized.length === 0)
|
|
105
|
+
throw new DeclarationError('unresolvable-symbol', `No declaration matches ${name}`);
|
|
106
|
+
if (normalized.length > 1)
|
|
107
|
+
throw new DeclarationError('ambiguous-symbol', `${name} is ambiguous`, normalized.map((item) => item.address));
|
|
108
|
+
return normalized[0];
|
|
109
|
+
}
|
|
110
|
+
fromSyntax(file, declaration) {
|
|
111
|
+
const location = declaration.kind === 'module' ? { path: relative(this.root, file), line: 0, col: 0 } : addressAt(this.source.read(file), declaration.nameSpan.start, relative(this.root, file));
|
|
112
|
+
return { address: inputAddress(location), location, file, kind: declaration.kind, name: declaration.name, qualifiedName: declaration.containerName ? `${declaration.containerName}.${declaration.name}` : declaration.name, nameSpan: declaration.nameSpan, span: declaration.span };
|
|
113
|
+
}
|
|
114
|
+
rows(cypher, parameters = {}) {
|
|
115
|
+
const store = this.storeFor();
|
|
116
|
+
if (!store)
|
|
117
|
+
throw new DeclarationError('not-in-program', 'The TypeScript index is not available yet.');
|
|
118
|
+
return query(store, cypher, parameters);
|
|
119
|
+
}
|
|
120
|
+
record(row) {
|
|
121
|
+
const location = { path: text(row, 'file'), line: number(row, 'line'), col: number(row, 'col') };
|
|
122
|
+
const file = path.join(this.root, location.path);
|
|
123
|
+
const source = this.source.read(file);
|
|
124
|
+
const name = location.line === 0 ? path.basename(file) : text(row, 'name');
|
|
125
|
+
const nameStart = location.line === 0 ? 0 : offsetAt(source, location.line, location.col);
|
|
126
|
+
const startLine = location.line === 0 ? 1 : number(row, 'endLine') - number(row, 'lines') + 1;
|
|
127
|
+
const start = location.line === 0 ? 0 : offsetAt(source, startLine, 1);
|
|
128
|
+
const end = location.line === 0 ? source.length : offsetAt(source, number(row, 'endLine') + 1, 1);
|
|
129
|
+
return { address: inputAddress(location), location, file, kind: text(row, 'kind'), name, qualifiedName: location.line === 0 ? name : text(row, 'qname'), nameSpan: { start: nameStart, end: nameStart + name.length }, span: { start, end } };
|
|
130
|
+
}
|
|
131
|
+
unresolvableAddress(address, file) {
|
|
132
|
+
const topLevel = this.rows('MATCH (d:Declaration) WHERE d.file = $file AND d.line > 0 AND d.container = $module RETURN d.at AS at, d.kind AS kind, d.qname AS qname ORDER BY d.line, d.col', { file, module: `${file}#<module>` });
|
|
133
|
+
const shown = topLevel.slice(0, 20);
|
|
134
|
+
const tail = topLevel.length > shown.length ? `\n+${topLevel.length - shown.length} more, run outline ${file}` : '';
|
|
135
|
+
return `${inputAddress(address)} does not resolve to a declaration.\nModule: ${file}:0:0\nTop-level declarations:\n${shown.map((row) => `${text(row, 'at')} ${text(row, 'kind')} ${text(row, 'qname')}`).join('\n')}${tail}`;
|
|
136
|
+
}
|
|
137
|
+
validatePosition(address, file) {
|
|
138
|
+
const lines = this.source.read(file).split('\n');
|
|
139
|
+
if (address.line < 1 || address.col < 1 || address.line > lines.length || address.col > lines[address.line - 1].length + 1)
|
|
140
|
+
throw new DeclarationError('invalid-address', `${inputAddress(address)} is outside the source text`);
|
|
141
|
+
}
|
|
142
|
+
notInProgram(file) { return new DeclarationError('not-in-program', `${file} exists but is not part of a TypeScript project under ${this.root}`); }
|
|
143
|
+
}
|
|
144
|
+
function parseAddress(value) { const match = /^(.*):(\d+):(\d+)$/.exec(value); return match ? { path: match[1], line: Number(match[2]), col: Number(match[3]) } : null; }
|
|
145
|
+
function inputAddress(address) { return `${address.path}:${address.line}:${address.col}`; }
|
|
146
|
+
function relative(root, file) { return path.relative(root, file).split(path.sep).join('/'); }
|
|
147
|
+
function offsetAt(source, line, col) { const lines = source.split('\n'); return lines.slice(0, Math.max(0, line - 1)).reduce((offset, value) => offset + value.length + 1, 0) + Math.max(0, col - 1); }
|
|
148
|
+
function identifierLengthAt(source, line, col) { return /^(?:[$_\p{ID_Start}]|\\u[0-9A-Fa-f]{4}|\\u\{[0-9A-Fa-f]+\})(?:[$_\p{ID_Continue}\u200C\u200D]|\\u[0-9A-Fa-f]{4}|\\u\{[0-9A-Fa-f]+\})*/u.exec(source.slice(offsetAt(source, line, col)))?.[0].length ?? 0; }
|
|
149
|
+
function addressAt(source, offset, file) { const before = source.slice(0, offset); return { path: file, line: before.split('\n').length, col: before.length - before.lastIndexOf('\n') }; }
|
|
150
|
+
function text(row, key) { return String(row[key] ?? ''); }
|
|
151
|
+
function number(row, key) { return Number(row[key]); }
|
|
152
|
+
function unique(records) { return [...new Map(records.map((record) => [record.address, record])).values()]; }
|
|
153
|
+
function columns() { return 'd.at AS at, d.name AS name, d.qname AS qname, d.kind AS kind, d.file AS file, d.line AS line, d.col AS col, d.endLine AS endLine, d.lines AS lines, d.container AS container'; }
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface PathFilters {
|
|
2
|
+
/** Restrict results to paths matching any glob. */
|
|
3
|
+
in?: string | readonly string[];
|
|
4
|
+
/** Remove results whose paths match any glob. */
|
|
5
|
+
exclude?: string | readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function matchesPathFilters(path: string, filters: PathFilters): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { matchesGlob } from './workspace.js';
|
|
2
|
+
export function matchesPathFilters(path, filters) {
|
|
3
|
+
const included = globs(filters.in);
|
|
4
|
+
const excluded = globs(filters.exclude);
|
|
5
|
+
return (included.length === 0 || included.some((glob) => matchesGlob(path, glob))) && !excluded.some((glob) => matchesGlob(path, glob));
|
|
6
|
+
}
|
|
7
|
+
function globs(value) {
|
|
8
|
+
return value === undefined ? [] : typeof value === 'string' ? [value] : value;
|
|
9
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PathFilters } from './receipt.js';
|
|
2
|
+
import { type Workspace } from './workspace.js';
|
|
3
|
+
export interface SearchOptions extends PathFilters {
|
|
4
|
+
limit?: number | null;
|
|
5
|
+
}
|
|
6
|
+
export interface SearchMatch {
|
|
7
|
+
path: string;
|
|
8
|
+
start: {
|
|
9
|
+
line: number;
|
|
10
|
+
col: number;
|
|
11
|
+
};
|
|
12
|
+
end: {
|
|
13
|
+
line: number;
|
|
14
|
+
col: number;
|
|
15
|
+
};
|
|
16
|
+
text: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SearchResult {
|
|
19
|
+
count: number;
|
|
20
|
+
items: SearchMatch[];
|
|
21
|
+
receipt: {
|
|
22
|
+
elapsed_ms: number;
|
|
23
|
+
excluded: number;
|
|
24
|
+
truncated: boolean;
|
|
25
|
+
limit: number | null;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Structural syntax search that stays outside the TypeScript resident. */
|
|
29
|
+
export declare function search(ws: Workspace, pattern: string, opts?: SearchOptions): Promise<SearchResult>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { findInFiles } from '@ast-grep/napi';
|
|
3
|
+
import { langFor, matchesGlob } from './workspace.js';
|
|
4
|
+
/** Structural syntax search that stays outside the TypeScript resident. */
|
|
5
|
+
export async function search(ws, pattern, opts = {}) {
|
|
6
|
+
const startedAt = performance.now();
|
|
7
|
+
const byLang = new Map();
|
|
8
|
+
for (const file of ws.files()) {
|
|
9
|
+
const lang = langFor(file);
|
|
10
|
+
if (lang !== null)
|
|
11
|
+
(byLang.get(lang) ?? byLang.set(lang, []).get(lang)).push(path.join(ws.root, file));
|
|
12
|
+
}
|
|
13
|
+
const matches = [];
|
|
14
|
+
for (const [lang, paths] of byLang) {
|
|
15
|
+
let seen = 0;
|
|
16
|
+
const count = await findInFiles(lang, { paths, matcher: { rule: { pattern } } }, (error, nodes) => {
|
|
17
|
+
seen++;
|
|
18
|
+
if (error)
|
|
19
|
+
return;
|
|
20
|
+
for (const node of nodes) {
|
|
21
|
+
const range = node.range();
|
|
22
|
+
matches.push({
|
|
23
|
+
path: ws.rel(node.getRoot().filename()),
|
|
24
|
+
start: { line: range.start.line + 1, col: range.start.column + 1 },
|
|
25
|
+
end: { line: range.end.line + 1, col: range.end.column + 1 },
|
|
26
|
+
text: node.text().split('\n', 1)[0]?.trim() ?? '',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
await drained(() => seen >= count);
|
|
31
|
+
}
|
|
32
|
+
matches.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : a.start.line - b.start.line || a.start.col - b.start.col));
|
|
33
|
+
const filtered = matches.filter((match) => matchesPathFilters(match.path, opts));
|
|
34
|
+
const limit = opts.limit ?? null;
|
|
35
|
+
const truncated = limit !== null && filtered.length > limit;
|
|
36
|
+
return { count: filtered.length, items: truncated ? filtered.slice(0, limit) : filtered, receipt: { elapsed_ms: Math.round(performance.now() - startedAt), excluded: matches.length - filtered.length, truncated, limit } };
|
|
37
|
+
}
|
|
38
|
+
function matchesPathFilters(file, filters) {
|
|
39
|
+
const included = values(filters.in);
|
|
40
|
+
const excluded = values(filters.exclude);
|
|
41
|
+
return (included.length === 0 || included.some((glob) => matchesGlob(file, glob))) && !excluded.some((glob) => matchesGlob(file, glob));
|
|
42
|
+
}
|
|
43
|
+
function values(value) { return value === undefined ? [] : typeof value === 'string' ? [value] : value; }
|
|
44
|
+
async function drained(done) {
|
|
45
|
+
while (!done())
|
|
46
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
47
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ProjectRegistry } from '../server/projects.js';
|
|
2
|
+
export interface SourceSpan {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
}
|
|
6
|
+
export interface SyntaxDeclaration {
|
|
7
|
+
name: string;
|
|
8
|
+
kind: string;
|
|
9
|
+
nameSpan: SourceSpan;
|
|
10
|
+
span: SourceSpan;
|
|
11
|
+
containerName?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Syntax-only reader backed by the resident TypeScript 7 snapshot. */
|
|
14
|
+
export declare class SourceReader {
|
|
15
|
+
private readonly projects;
|
|
16
|
+
constructor(projects: Pick<ProjectRegistry, 'refreshFile' | 'sourceFile' | 'anySourceFile'>);
|
|
17
|
+
read(file: string): string;
|
|
18
|
+
declarationAt(file: string, line: number, offset: number): SyntaxDeclaration | null;
|
|
19
|
+
declarationNameSpanAt(file: string, line: number, offset: number): SourceSpan | null;
|
|
20
|
+
declarationSpanAt(file: string, line: number, offset: number): SourceSpan | null;
|
|
21
|
+
declarationForSpan(file: string, start: {
|
|
22
|
+
line: number;
|
|
23
|
+
offset: number;
|
|
24
|
+
}): SyntaxDeclaration | null;
|
|
25
|
+
ownerForSpan(file: string, start: {
|
|
26
|
+
line: number;
|
|
27
|
+
offset: number;
|
|
28
|
+
}): SyntaxDeclaration;
|
|
29
|
+
module(file: string): SyntaxDeclaration;
|
|
30
|
+
private parse;
|
|
31
|
+
private tokenAt;
|
|
32
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as ts from '../ts7/ast.js';
|
|
4
|
+
/** Syntax-only reader backed by the resident TypeScript 7 snapshot. */
|
|
5
|
+
export class SourceReader {
|
|
6
|
+
projects;
|
|
7
|
+
constructor(projects) { this.projects = projects; }
|
|
8
|
+
read(file) { return readFileSync(file, 'utf8'); }
|
|
9
|
+
declarationAt(file, line, offset) {
|
|
10
|
+
const source = this.parse(file);
|
|
11
|
+
return declarationAt(this.tokenAt(source, source.getPositionOfLineAndCharacter(line - 1, offset - 1)), source);
|
|
12
|
+
}
|
|
13
|
+
declarationNameSpanAt(file, line, offset) {
|
|
14
|
+
const source = this.parse(file);
|
|
15
|
+
const declaration = indexedDeclarationFor(this.tokenAt(source, source.getPositionOfLineAndCharacter(line - 1, offset - 1)));
|
|
16
|
+
const name = declaration && declaration.name;
|
|
17
|
+
return name ? { start: name.getStart(source), end: name.getEnd() } : null;
|
|
18
|
+
}
|
|
19
|
+
declarationSpanAt(file, line, offset) {
|
|
20
|
+
const source = this.parse(file);
|
|
21
|
+
const declaration = indexedDeclarationFor(this.tokenAt(source, source.getPositionOfLineAndCharacter(line - 1, offset - 1)));
|
|
22
|
+
return declaration ? { start: declaration.getStart(source), end: declaration.getEnd() } : null;
|
|
23
|
+
}
|
|
24
|
+
declarationForSpan(file, start) { return this.declarationAt(file, start.line, start.offset); }
|
|
25
|
+
ownerForSpan(file, start) {
|
|
26
|
+
const source = this.parse(file);
|
|
27
|
+
return ownerAt(this.tokenAt(source, source.getPositionOfLineAndCharacter(start.line - 1, start.offset - 1)), source) ?? moduleFor(source);
|
|
28
|
+
}
|
|
29
|
+
module(file) { return moduleFor(this.parse(file)); }
|
|
30
|
+
parse(file) {
|
|
31
|
+
this.projects.refreshFile(file);
|
|
32
|
+
const source = this.projects.sourceFile(file) ?? this.projects.anySourceFile(file);
|
|
33
|
+
if (!source)
|
|
34
|
+
throw new Error(`${file} is not loaded in the TypeScript 7 snapshot.`);
|
|
35
|
+
return source;
|
|
36
|
+
}
|
|
37
|
+
tokenAt(source, position) {
|
|
38
|
+
let current = source;
|
|
39
|
+
for (;;) {
|
|
40
|
+
const child = current.forEachChild((candidate) => position >= candidate.getFullStart() && position < candidate.getEnd() ? candidate : undefined);
|
|
41
|
+
if (!child)
|
|
42
|
+
return current;
|
|
43
|
+
current = child;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function declarationAt(node, source) {
|
|
48
|
+
let current = node;
|
|
49
|
+
while (current) {
|
|
50
|
+
const declaration = declarationFor(current);
|
|
51
|
+
if (declaration)
|
|
52
|
+
return describe(declaration, source);
|
|
53
|
+
current = current.parent;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function ownerAt(node, source) {
|
|
58
|
+
let current = node;
|
|
59
|
+
while (current) {
|
|
60
|
+
const declaration = declarationFor(current);
|
|
61
|
+
if (declaration && namedCallable(declaration))
|
|
62
|
+
return describe(declaration, source);
|
|
63
|
+
current = current.parent;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function indexedDeclarationFor(node) {
|
|
68
|
+
let current = node;
|
|
69
|
+
while (current) {
|
|
70
|
+
if (ts.isVariableDeclaration(current) || ts.isFunctionDeclaration(current) || ts.isFunctionExpression(current) || ts.isClassDeclaration(current) || ts.isClassExpression(current) || ts.isInterfaceDeclaration(current) || ts.isTypeAliasDeclaration(current) || ts.isEnumDeclaration(current) || ts.isModuleDeclaration(current) || ts.isMethodDeclaration(current) || current.kind === ts.SyntaxKind.MethodSignature || ts.isPropertyDeclaration(current) || current.kind === ts.SyntaxKind.PropertySignature || ts.isGetAccessorDeclaration(current) || ts.isSetAccessorDeclaration(current) || ts.isConstructorDeclaration(current) || ts.isParameterDeclaration(current) || ts.isBindingElement(current) || ts.isImportSpecifier(current) || ts.isExportSpecifier(current) || ts.isExportAssignment(current) || ts.isArrowFunction(current))
|
|
71
|
+
return current;
|
|
72
|
+
current = current.parent;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function declarationFor(node) {
|
|
77
|
+
if (ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isMethodDeclaration(node) || ts.isPropertyDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) || ts.isParameterDeclaration(node) || ts.isImportSpecifier(node) || ts.isExportSpecifier(node))
|
|
78
|
+
return node;
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function namedCallable(node) {
|
|
82
|
+
if (ts.isVariableDeclaration(node) || ts.isPropertyDeclaration(node))
|
|
83
|
+
return !!node.name && !!node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer));
|
|
84
|
+
return (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) && !!node.name;
|
|
85
|
+
}
|
|
86
|
+
function describe(node, source) {
|
|
87
|
+
const name = node.name;
|
|
88
|
+
if (!name || !ts.isIdentifier(name) && !ts.isPrivateIdentifier(name) && !ts.isStringLiteral(name))
|
|
89
|
+
return null;
|
|
90
|
+
const parent = node.parent;
|
|
91
|
+
const container = parent && (ts.isClassDeclaration(parent) || ts.isInterfaceDeclaration(parent) || ts.isEnumDeclaration(parent)) && parent.name ? parent.name.text : undefined;
|
|
92
|
+
return { name: name.text, kind: ts.SyntaxKind[node.kind] ?? 'unknown', nameSpan: { start: name.getStart(source), end: name.getEnd() }, span: { start: node.getStart(source), end: node.getEnd() }, containerName: container };
|
|
93
|
+
}
|
|
94
|
+
function moduleFor(source) {
|
|
95
|
+
return { name: path.basename(source.fileName), kind: 'module', nameSpan: { start: 0, end: 0 }, span: { start: 0, end: source.getEnd() } };
|
|
96
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Lang } from '@ast-grep/napi';
|
|
2
|
+
/** refs/callers/impact were asked for a file with no TypeScript project around it. */
|
|
3
|
+
export declare class NoProject extends Error {
|
|
4
|
+
}
|
|
5
|
+
export declare class Workspace {
|
|
6
|
+
readonly root: string;
|
|
7
|
+
/** The tsconfig.json at the root, when the root was chosen because of one. */
|
|
8
|
+
readonly tsconfig: string | null;
|
|
9
|
+
/** Which marker chose the root; `none` means `dir` itself was used because nothing above it is a project. */
|
|
10
|
+
readonly marker: 'tsconfig.json' | 'package.json' | '.git' | 'none';
|
|
11
|
+
private fileList;
|
|
12
|
+
constructor(root: string, tsconfig: string | null, marker: Workspace['marker']);
|
|
13
|
+
/** Path relative to the root, `/`-separated. */
|
|
14
|
+
rel(abs: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Source files (TS/JS family) in the workspace, relative to the root.
|
|
17
|
+
* Uses `git ls-files` (tracked + untracked, honoring .gitignore) when the root is in a git repo,
|
|
18
|
+
* else a directory walk that skips node_modules/dist/build/.git.
|
|
19
|
+
*/
|
|
20
|
+
files(globs?: string | readonly string[]): string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the workspace containing `dir`: the nearest ancestor holding a tsconfig.json,
|
|
24
|
+
* else package.json, else a .git directory, else `dir` itself.
|
|
25
|
+
*/
|
|
26
|
+
export declare function openWorkspace(dir?: string): Workspace;
|
|
27
|
+
export declare function langFor(file: string): Lang | null;
|
|
28
|
+
export declare function matchesGlob(file: string, glob: string): boolean;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Lang } from '@ast-grep/napi';
|
|
5
|
+
const SOURCE_EXT = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
|
|
6
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git', 'coverage']);
|
|
7
|
+
/** refs/callers/impact were asked for a file with no TypeScript project around it. */
|
|
8
|
+
export class NoProject extends Error {
|
|
9
|
+
}
|
|
10
|
+
export class Workspace {
|
|
11
|
+
root;
|
|
12
|
+
/** The tsconfig.json at the root, when the root was chosen because of one. */
|
|
13
|
+
tsconfig;
|
|
14
|
+
/** Which marker chose the root; `none` means `dir` itself was used because nothing above it is a project. */
|
|
15
|
+
marker;
|
|
16
|
+
fileList = null;
|
|
17
|
+
constructor(root, tsconfig, marker) {
|
|
18
|
+
this.root = root;
|
|
19
|
+
this.tsconfig = tsconfig;
|
|
20
|
+
this.marker = marker;
|
|
21
|
+
}
|
|
22
|
+
/** Path relative to the root, `/`-separated. */
|
|
23
|
+
rel(abs) {
|
|
24
|
+
return path.relative(this.root, abs).split(path.sep).join('/');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Source files (TS/JS family) in the workspace, relative to the root.
|
|
28
|
+
* Uses `git ls-files` (tracked + untracked, honoring .gitignore) when the root is in a git repo,
|
|
29
|
+
* else a directory walk that skips node_modules/dist/build/.git.
|
|
30
|
+
*/
|
|
31
|
+
files(globs) {
|
|
32
|
+
if (!this.fileList)
|
|
33
|
+
this.fileList = listFiles(this.root);
|
|
34
|
+
if (!globs)
|
|
35
|
+
return this.fileList;
|
|
36
|
+
const patterns = typeof globs === 'string' ? [globs] : globs;
|
|
37
|
+
return this.fileList.filter((f) => patterns.some((glob) => matchesGlob(f, glob)));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the workspace containing `dir`: the nearest ancestor holding a tsconfig.json,
|
|
42
|
+
* else package.json, else a .git directory, else `dir` itself.
|
|
43
|
+
*/
|
|
44
|
+
export function openWorkspace(dir = process.cwd()) {
|
|
45
|
+
const start = path.resolve(dir);
|
|
46
|
+
for (const marker of ['tsconfig.json', 'package.json', '.git']) {
|
|
47
|
+
let cur = start;
|
|
48
|
+
for (;;) {
|
|
49
|
+
const candidate = path.join(cur, marker);
|
|
50
|
+
if (existsSync(candidate)) {
|
|
51
|
+
return new Workspace(cur, marker === 'tsconfig.json' ? candidate : nearestTsconfig(cur, cur), marker);
|
|
52
|
+
}
|
|
53
|
+
const parent = path.dirname(cur);
|
|
54
|
+
if (parent === cur)
|
|
55
|
+
break;
|
|
56
|
+
cur = parent;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return new Workspace(start, null, 'none');
|
|
60
|
+
}
|
|
61
|
+
function nearestTsconfig(dir, stopAt) {
|
|
62
|
+
let current = dir;
|
|
63
|
+
for (;;) {
|
|
64
|
+
const candidate = path.join(current, 'tsconfig.json');
|
|
65
|
+
if (existsSync(candidate))
|
|
66
|
+
return candidate;
|
|
67
|
+
if (current === stopAt)
|
|
68
|
+
return null;
|
|
69
|
+
const parent = path.dirname(current);
|
|
70
|
+
if (parent === current)
|
|
71
|
+
return null;
|
|
72
|
+
current = parent;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function langFor(file) {
|
|
76
|
+
switch (path.extname(file)) {
|
|
77
|
+
case '.ts':
|
|
78
|
+
case '.mts':
|
|
79
|
+
case '.cts':
|
|
80
|
+
return Lang.TypeScript;
|
|
81
|
+
case '.tsx':
|
|
82
|
+
return Lang.Tsx;
|
|
83
|
+
case '.js':
|
|
84
|
+
case '.mjs':
|
|
85
|
+
case '.cjs':
|
|
86
|
+
case '.jsx':
|
|
87
|
+
return Lang.JavaScript;
|
|
88
|
+
default:
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function matchesGlob(file, glob) {
|
|
93
|
+
// A bare directory matches as a prefix; a bare filename matches in any directory.
|
|
94
|
+
if (!/[*?[{]/.test(glob)) {
|
|
95
|
+
const normalized = glob.replace(/\/$/, '');
|
|
96
|
+
return file === normalized || file.startsWith(normalized + '/') || (!normalized.includes('/') && file.split('/').includes(normalized));
|
|
97
|
+
}
|
|
98
|
+
const pattern = glob.includes('/') ? glob : `**/${glob}`;
|
|
99
|
+
return path.matchesGlob(file, pattern);
|
|
100
|
+
}
|
|
101
|
+
function listFiles(root) {
|
|
102
|
+
let out;
|
|
103
|
+
try {
|
|
104
|
+
const raw = execFileSync('git', ['ls-files', '-co', '--exclude-standard', '-z'], {
|
|
105
|
+
cwd: root,
|
|
106
|
+
encoding: 'utf8',
|
|
107
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
108
|
+
maxBuffer: 1 << 28,
|
|
109
|
+
});
|
|
110
|
+
out = raw.split('\0').filter(Boolean);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
out = [];
|
|
114
|
+
walk(root, '', out);
|
|
115
|
+
}
|
|
116
|
+
return out.filter((f) => SOURCE_EXT.has(path.extname(f)) && !f.split('/').some((seg) => SKIP_DIRS.has(seg)) && existsSync(path.join(root, f)));
|
|
117
|
+
}
|
|
118
|
+
function walk(root, rel, out) {
|
|
119
|
+
for (const entry of readdirSync(path.join(root, rel), { withFileTypes: true })) {
|
|
120
|
+
const relPath = rel ? `${rel}/${entry.name}` : entry.name;
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
if (!SKIP_DIRS.has(entry.name))
|
|
123
|
+
walk(root, relPath, out);
|
|
124
|
+
}
|
|
125
|
+
else if (entry.isFile() || (entry.isSymbolicLink() && safeIsFile(path.join(root, relPath)))) {
|
|
126
|
+
out.push(relPath);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function safeIsFile(p) {
|
|
131
|
+
try {
|
|
132
|
+
return statSync(p).isFile();
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type LoadReceipt, type StoreGraph } from '../store/load.js';
|
|
2
|
+
import { Store } from '../store/store.js';
|
|
3
|
+
import { Ts7Host } from '../ts7/api.js';
|
|
4
|
+
import { ConfiguredProjects } from '../ts7/projects.js';
|
|
5
|
+
import type { IndexGraph } from './graph.js';
|
|
6
|
+
import { type HeritageGraph } from './heritage.js';
|
|
7
|
+
export interface BuildReceipt extends LoadReceipt {
|
|
8
|
+
internalSites: number;
|
|
9
|
+
unresolvedOccurrences: number;
|
|
10
|
+
dispatchCalls: number;
|
|
11
|
+
durationMs: number;
|
|
12
|
+
}
|
|
13
|
+
export interface BuiltIndex {
|
|
14
|
+
graph: IndexGraph;
|
|
15
|
+
heritage: HeritageGraph['relationships'];
|
|
16
|
+
receipt: BuildReceipt;
|
|
17
|
+
}
|
|
18
|
+
/** Walks the configured project union, adds heritage facts, and atomically replaces the persisted graph. */
|
|
19
|
+
export declare function buildIndex(root: string, program: Ts7Host, projects: ConfiguredProjects, store: Store): BuiltIndex;
|
|
20
|
+
/** Rehydrates an unchanged persisted graph without re-walking the TypeScript program. */
|
|
21
|
+
export declare function reuseIndex(store: Store): BuiltIndex;
|
|
22
|
+
export declare function storeGraph(index: HeritageGraph): StoreGraph;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { loadGraph, readGraph } from '../store/load.js';
|
|
3
|
+
import { metadataFor, readMetadata, writeMetadata } from '../store/meta.js';
|
|
4
|
+
import { addHeritage } from './heritage.js';
|
|
5
|
+
import { walkIndex } from './walk.js';
|
|
6
|
+
/** Walks the configured project union, adds heritage facts, and atomically replaces the persisted graph. */
|
|
7
|
+
export function buildIndex(root, program, projects, store) {
|
|
8
|
+
const started = performance.now();
|
|
9
|
+
const walked = walkIndex(root, program, projects);
|
|
10
|
+
const enriched = addHeritage(root, program, projects, walked);
|
|
11
|
+
const loaded = loadGraph(store, storeGraph(enriched));
|
|
12
|
+
const durationMs = performance.now() - started;
|
|
13
|
+
writeMetadata(store.path, metadataFor(root, projects, headCommit(root), durationMs));
|
|
14
|
+
return {
|
|
15
|
+
graph: enriched.graph,
|
|
16
|
+
heritage: enriched.relationships,
|
|
17
|
+
receipt: {
|
|
18
|
+
...loaded,
|
|
19
|
+
internalSites: enriched.graph.internalSites,
|
|
20
|
+
unresolvedOccurrences: enriched.graph.unresolvedOccurrences,
|
|
21
|
+
dispatchCalls: enriched.dispatchCalls,
|
|
22
|
+
durationMs,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Rehydrates an unchanged persisted graph without re-walking the TypeScript program. */
|
|
27
|
+
export function reuseIndex(store) {
|
|
28
|
+
const persisted = readGraph(store);
|
|
29
|
+
const metadata = readMetadata(store.path);
|
|
30
|
+
if (!metadata)
|
|
31
|
+
throw new Error('reuseIndex requires persisted store metadata; canReuse must pass first.');
|
|
32
|
+
const durationMs = metadata.indexDurationMs;
|
|
33
|
+
const declarations = new Map(persisted.declarations.map((declaration) => [declaration.key, declaration]));
|
|
34
|
+
const externals = new Map(persisted.externals.map((external) => [external.key, external]));
|
|
35
|
+
const relationships = persisted.relationships.filter((relationship) => ['CONTAINS', 'REFERENCES', 'CALLS', 'TYPE_MENTION', 'IMPORTS', 'EXPORTS'].includes(relationship.type)).map(({ target: _target, ...relationship }) => relationship);
|
|
36
|
+
const heritage = persisted.relationships.filter((relationship) => relationship.type === 'IMPLEMENTS' || relationship.type === 'EXTENDS').map(({ target: _target, ...relationship }) => relationship);
|
|
37
|
+
const files = new Map();
|
|
38
|
+
for (const declaration of declarations.values())
|
|
39
|
+
files.set(declaration.file, { declarations: [], relationships: [] });
|
|
40
|
+
for (const declaration of declarations.values())
|
|
41
|
+
files.get(declaration.file).declarations.push(declaration.key);
|
|
42
|
+
for (const relationship of relationships) {
|
|
43
|
+
const owner = declarations.get(relationship.from);
|
|
44
|
+
if (owner)
|
|
45
|
+
files.get(owner.file).relationships.push(relationship);
|
|
46
|
+
}
|
|
47
|
+
const counts = Object.fromEntries(['CONTAINS', 'REFERENCES', 'CALLS', 'IMPLEMENTS', 'EXTENDS', 'TYPE_MENTION', 'IMPORTS', 'EXPORTS', 'CHANGED', 'RENAMED_FROM'].map((type) => [type, 0]));
|
|
48
|
+
for (const relationship of persisted.relationships)
|
|
49
|
+
counts[relationship.type]++;
|
|
50
|
+
return {
|
|
51
|
+
graph: { declarations, externals, files, relationships, internalSites: 0, unresolvedOccurrences: 0 },
|
|
52
|
+
heritage,
|
|
53
|
+
receipt: { declarations: persisted.declarations.length, externals: persisted.externals.length, commits: persisted.commits?.length ?? 0, relationships: counts, internalSites: 0, unresolvedOccurrences: 0, dispatchCalls: persisted.relationships.filter((relationship) => relationship.type === 'CALLS' && relationship.dispatch === true).length, durationMs },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function storeGraph(index) {
|
|
57
|
+
return {
|
|
58
|
+
declarations: [...index.graph.declarations.values()].map(declarationRow),
|
|
59
|
+
externals: [...index.graph.externals.values()],
|
|
60
|
+
relationships: [
|
|
61
|
+
...index.graph.relationships.map((relationship) => relationshipRow(relationship, index.graph)),
|
|
62
|
+
...index.relationships.map((relationship) => ({ ...relationship, target: 'Declaration' })),
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function declarationRow(node) {
|
|
67
|
+
return { ...node, container: node.container };
|
|
68
|
+
}
|
|
69
|
+
function relationshipRow(relationship, graph) {
|
|
70
|
+
return { ...relationship, target: graph.externals.has(relationship.to) ? 'External' : 'Declaration' };
|
|
71
|
+
}
|
|
72
|
+
function headCommit(root) {
|
|
73
|
+
try {
|
|
74
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return 'unknown';
|
|
78
|
+
}
|
|
79
|
+
}
|