codedeep-mcp 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 +177 -0
- package/dist/config.js +223 -0
- package/dist/git/analyzer.js +177 -0
- package/dist/git/git-service.js +568 -0
- package/dist/git/head-watcher.js +113 -0
- package/dist/git/runner.js +204 -0
- package/dist/index.js +138 -0
- package/dist/indexer/code-index.js +1801 -0
- package/dist/indexer/complexity.js +633 -0
- package/dist/indexer/extractor.js +354 -0
- package/dist/indexer/languages/cpp.js +934 -0
- package/dist/indexer/languages/csharp.js +854 -0
- package/dist/indexer/languages/dart.js +777 -0
- package/dist/indexer/languages/go.js +665 -0
- package/dist/indexer/languages/java.js +507 -0
- package/dist/indexer/languages/kotlin.js +709 -0
- package/dist/indexer/languages/objc.js +397 -0
- package/dist/indexer/languages/php.js +771 -0
- package/dist/indexer/languages/python.js +455 -0
- package/dist/indexer/languages/ruby.js +697 -0
- package/dist/indexer/languages/rust.js +754 -0
- package/dist/indexer/languages/swift.js +691 -0
- package/dist/indexer/languages/typescript.js +485 -0
- package/dist/indexer/parser.js +175 -0
- package/dist/indexer/pipeline.js +342 -0
- package/dist/indexer/scanner.js +279 -0
- package/dist/indexer/watcher.js +353 -0
- package/dist/logger.js +16 -0
- package/dist/server.js +170 -0
- package/dist/tools/common.js +207 -0
- package/dist/tools/find-references.js +224 -0
- package/dist/tools/find-symbol.js +94 -0
- package/dist/tools/get-context.js +370 -0
- package/dist/tools/impact.js +218 -0
- package/dist/tools/overview.js +482 -0
- package/dist/tools/search-structure.js +303 -0
- package/dist/types.js +61 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-dart.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-objc.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +67 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { basename, extname, join, resolve } from 'node:path';
|
|
3
|
+
import { ENTRY_POINT_FILENAME_RE, isClassMember, zeroSymbolsByKind, } from '../indexer/code-index.js';
|
|
4
|
+
import { compareShallowFirst } from '../indexer/scanner.js';
|
|
5
|
+
import { errMsg, log } from '../logger.js';
|
|
6
|
+
import { LANGUAGE_UNKNOWN, } from '../types.js';
|
|
7
|
+
import { BEHAVIORAL_TAG, formatComplexityMetrics, INDEXING_BANNER, plural, textResponse, } from './common.js';
|
|
8
|
+
const MAX_DIR_GROUPS = 7;
|
|
9
|
+
const MAX_KINDS_PER_GROUP = 3;
|
|
10
|
+
const MAX_ENTRY_POINTS = 15;
|
|
11
|
+
const MAX_OTHER_EXTENSIONS = 5;
|
|
12
|
+
const MAX_HOTSPOTS = 10;
|
|
13
|
+
const MAX_RISK_HOTSPOTS = 10;
|
|
14
|
+
// Without this, monorepos with many packages/*/index.ts (or many
|
|
15
|
+
// __init__.py) saturate MAX_ENTRY_POINTS alphabetically and hide the real
|
|
16
|
+
// startup file. Tested only against names that already passed
|
|
17
|
+
// ENTRY_POINT_FILENAME_RE, so a stem-only check is sufficient.
|
|
18
|
+
const BARREL_ENTRY_RE = /^(index|__init__)\./i;
|
|
19
|
+
const LANGUAGE_DISPLAY = {
|
|
20
|
+
typescript: 'TypeScript',
|
|
21
|
+
tsx: 'TSX',
|
|
22
|
+
javascript: 'JavaScript',
|
|
23
|
+
python: 'Python',
|
|
24
|
+
java: 'Java',
|
|
25
|
+
go: 'Go',
|
|
26
|
+
rust: 'Rust',
|
|
27
|
+
swift: 'Swift',
|
|
28
|
+
kotlin: 'Kotlin',
|
|
29
|
+
dart: 'Dart',
|
|
30
|
+
csharp: 'C#',
|
|
31
|
+
php: 'PHP',
|
|
32
|
+
ruby: 'Ruby',
|
|
33
|
+
cpp: 'C++',
|
|
34
|
+
c: 'C',
|
|
35
|
+
objc: 'Objective-C',
|
|
36
|
+
};
|
|
37
|
+
const KIND_PLURAL = {
|
|
38
|
+
function: 'functions',
|
|
39
|
+
class: 'classes',
|
|
40
|
+
interface: 'interfaces',
|
|
41
|
+
type: 'types',
|
|
42
|
+
variable: 'variables',
|
|
43
|
+
method: 'methods',
|
|
44
|
+
module: 'modules',
|
|
45
|
+
enum: 'enums',
|
|
46
|
+
};
|
|
47
|
+
export async function runOverview(args, deps) {
|
|
48
|
+
try {
|
|
49
|
+
const projectRoot = deps.config.projectRoot;
|
|
50
|
+
if (args.path && resolve(args.path) !== projectRoot) {
|
|
51
|
+
return textResponse(`Error: path "${args.path}" does not match configured project root "${projectRoot}". Multi-root workspaces are not yet supported.`);
|
|
52
|
+
}
|
|
53
|
+
const stats = deps.index.getStats();
|
|
54
|
+
const allFiles = deps.index.getAllFiles();
|
|
55
|
+
const lines = [];
|
|
56
|
+
if (!deps.indexer.ready) {
|
|
57
|
+
lines.push(INDEXING_BANNER, '');
|
|
58
|
+
}
|
|
59
|
+
lines.push(`## Project: ${basename(projectRoot)}`, '');
|
|
60
|
+
const unknownCount = stats.filesByLanguage[LANGUAGE_UNKNOWN] ?? 0;
|
|
61
|
+
const recognizedTotal = stats.totalFiles - unknownCount;
|
|
62
|
+
lines.push('### Languages');
|
|
63
|
+
if (recognizedTotal === 0) {
|
|
64
|
+
lines.push('- (no source files indexed)');
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const langs = Object.entries(stats.filesByLanguage)
|
|
68
|
+
.filter(([lang]) => lang !== LANGUAGE_UNKNOWN)
|
|
69
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
70
|
+
for (const [lang, count] of langs) {
|
|
71
|
+
const pct = Math.round((count / recognizedTotal) * 100);
|
|
72
|
+
lines.push(`- ${displayLanguage(lang)}: ${count} ${plural('file', count)} (${pct}%)`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
lines.push('');
|
|
76
|
+
if (unknownCount > 0) {
|
|
77
|
+
lines.push('### Other files');
|
|
78
|
+
const topExts = topUnknownExtensions(allFiles, MAX_OTHER_EXTENSIONS);
|
|
79
|
+
const detail = topExts.length > 0 ? ` (${topExts.join(', ')})` : '';
|
|
80
|
+
lines.push(`- ${unknownCount} ${plural('file', unknownCount)} not parsed${detail}`);
|
|
81
|
+
lines.push('');
|
|
82
|
+
}
|
|
83
|
+
lines.push('### Structure');
|
|
84
|
+
const groups = groupFilesByDirectory(allFiles, deps.index);
|
|
85
|
+
if (groups.length === 0) {
|
|
86
|
+
lines.push('- (no files indexed)');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
for (const g of groups) {
|
|
90
|
+
const kindParts = g.topKinds.map((k) => `${k.count} ${pluralKind(k.kind, k.count)}`);
|
|
91
|
+
const kindSuffix = kindParts.length > 0 ? ` (${kindParts.join(', ')})` : '';
|
|
92
|
+
lines.push(`- ${g.dir} — ${g.fileCount} ${plural('file', g.fileCount)}${kindSuffix}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
lines.push('');
|
|
96
|
+
lines.push('### Entry Points');
|
|
97
|
+
const entries = await collectEntryPoints(deps.index, allFiles, projectRoot);
|
|
98
|
+
if (entries.length === 0) {
|
|
99
|
+
lines.push('- (none detected)');
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
for (const e of entries) {
|
|
103
|
+
if (e.summary) {
|
|
104
|
+
lines.push(`- ${e.file} — ${e.summary}`);
|
|
105
|
+
}
|
|
106
|
+
else if (e.symbol !== undefined && e.line !== undefined) {
|
|
107
|
+
lines.push(`- ${e.file}:${e.line} — ${e.symbol}`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
lines.push(`- ${e.file}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
lines.push('');
|
|
115
|
+
lines.push('### Symbols');
|
|
116
|
+
const kindLine = formatSymbolKinds(stats.symbolsByKind);
|
|
117
|
+
if (kindLine)
|
|
118
|
+
lines.push(`- ${kindLine}`);
|
|
119
|
+
lines.push(`- ${stats.totalFiles} ${plural('file', stats.totalFiles)} indexed, ${stats.totalSymbols} total ${plural('symbol', stats.totalSymbols)}`);
|
|
120
|
+
appendGitSections(lines, await collectGitData(deps));
|
|
121
|
+
return textResponse(lines.join('\n'));
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
return textResponse(`Error: ${errMsg(err)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Defensive catch at the tool boundary: GitService promises not to
|
|
128
|
+
// throw, but a git failure must never break overview output.
|
|
129
|
+
async function collectGitData(deps) {
|
|
130
|
+
let branch = null;
|
|
131
|
+
try {
|
|
132
|
+
branch = await deps.git.branchSummary();
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
branch = null;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
branch,
|
|
139
|
+
hotspots: deps.index.getHotspots(MAX_HOTSPOTS),
|
|
140
|
+
// Empty off-git (getRiskHotspots gates on getGitMeta) — the section is
|
|
141
|
+
// then omitted, matching the silent-omission contract below.
|
|
142
|
+
riskHotspots: deps.index.getRiskHotspots(MAX_RISK_HOTSPOTS),
|
|
143
|
+
// Label with the window that PRODUCED the data (gitMeta provenance),
|
|
144
|
+
// not the live config: after a gitWindow change, persisted counts
|
|
145
|
+
// keep their true label until the re-analysis lands.
|
|
146
|
+
windowDays: deps.index.getGitMeta()?.windowDays ?? deps.config.gitWindow,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// Both sections vanish entirely outside git repos (and before the first
|
|
150
|
+
// analysis lands) — silent omission is the degradation contract, never a
|
|
151
|
+
// placeholder. Hotspots come from the persisted index, so a warm start
|
|
152
|
+
// shows them immediately, even while the indexing banner is up.
|
|
153
|
+
function appendGitSections(lines, data) {
|
|
154
|
+
if (data.branch !== null) {
|
|
155
|
+
lines.push('', `### Branch ${BEHAVIORAL_TAG}`, formatBranchLine(data.branch));
|
|
156
|
+
}
|
|
157
|
+
if (data.hotspots.length > 0) {
|
|
158
|
+
lines.push('', `### Hotspots (last ${data.windowDays} days) ${BEHAVIORAL_TAG}`);
|
|
159
|
+
for (const h of data.hotspots) {
|
|
160
|
+
lines.push(`- ${h.path} — ${h.commits} ${plural('commit', h.commits)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Churn × coupling × complexity: the file's most-coupled symbol crossed with
|
|
164
|
+
// its commit frequency, refined by that offender's complexity. Empty (and so
|
|
165
|
+
// omitted) off-git, where the product has no churn factor — the same silent-
|
|
166
|
+
// omission contract as the sections above.
|
|
167
|
+
if (data.riskHotspots.length > 0) {
|
|
168
|
+
lines.push('', `### Risk Hotspots (churn × coupling × complexity) ${BEHAVIORAL_TAG}`);
|
|
169
|
+
for (const r of data.riskHotspots) {
|
|
170
|
+
// No `()` after the offender — it can be a class/variable, not a function.
|
|
171
|
+
// "references" (not "callers") because fanIn is reference-granular, unlike
|
|
172
|
+
// the distinct-caller blast count; a trailing `+` flags a capped walk.
|
|
173
|
+
// The offender's complexity is appended tag-less (fanIn/blast are already
|
|
174
|
+
// rendered tag-less under the one [behavioral] heading); omitted entirely
|
|
175
|
+
// for a trivial offender so the line keeps its churn × coupling shape.
|
|
176
|
+
const complexity = formatComplexityMetrics(r);
|
|
177
|
+
lines.push(`- ${r.file} — ${r.symbol} — ${r.churn} ${plural('commit', r.churn)} × ` +
|
|
178
|
+
`${r.fanIn} ${plural('reference', r.fanIn)} ` +
|
|
179
|
+
`(blast radius ${r.blast.callers}${r.blast.truncated ? '+' : ''} across ${r.blast.files} ${plural('file', r.blast.files)})` +
|
|
180
|
+
(complexity ? ` — ${complexity}` : ''));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function formatBranchLine(s) {
|
|
185
|
+
if (s.defaultBranch !== null && s.branch === s.defaultBranch) {
|
|
186
|
+
return `- ${s.branch} (default branch)`;
|
|
187
|
+
}
|
|
188
|
+
if (s.defaultBranch === null || s.ahead === null) {
|
|
189
|
+
return `- ${s.branch}`;
|
|
190
|
+
}
|
|
191
|
+
const files = s.changedFiles === null
|
|
192
|
+
? ''
|
|
193
|
+
: `, ${s.changedFiles.length} ${plural('file', s.changedFiles.length)} changed on branch`;
|
|
194
|
+
return `- ${s.branch} — ${s.ahead} ${plural('commit', s.ahead)} ahead of ${s.defaultBranch}${files}`;
|
|
195
|
+
}
|
|
196
|
+
function displayLanguage(lang) {
|
|
197
|
+
return LANGUAGE_DISPLAY[lang] ?? capitalize(lang);
|
|
198
|
+
}
|
|
199
|
+
function topUnknownExtensions(files, limit) {
|
|
200
|
+
const counts = new Map();
|
|
201
|
+
for (const f of files) {
|
|
202
|
+
if (f.language !== LANGUAGE_UNKNOWN)
|
|
203
|
+
continue;
|
|
204
|
+
const ext = extname(f.path).toLowerCase();
|
|
205
|
+
const key = ext === '' ? '(no ext)' : ext;
|
|
206
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
207
|
+
}
|
|
208
|
+
return [...counts.entries()]
|
|
209
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
210
|
+
.slice(0, limit)
|
|
211
|
+
.map(([ext]) => ext);
|
|
212
|
+
}
|
|
213
|
+
function capitalize(s) {
|
|
214
|
+
return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
|
|
215
|
+
}
|
|
216
|
+
function pluralKind(kind, count) {
|
|
217
|
+
return count === 1 ? kind : KIND_PLURAL[kind];
|
|
218
|
+
}
|
|
219
|
+
function sortedKindCounts(kinds) {
|
|
220
|
+
return Object.entries(kinds)
|
|
221
|
+
.filter(([, c]) => c > 0)
|
|
222
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
223
|
+
}
|
|
224
|
+
function groupKey(filePath) {
|
|
225
|
+
const parts = filePath.split('/');
|
|
226
|
+
if (parts.length <= 1)
|
|
227
|
+
return '(root)';
|
|
228
|
+
const dirParts = parts.slice(0, -1);
|
|
229
|
+
if (dirParts.length === 1)
|
|
230
|
+
return dirParts[0];
|
|
231
|
+
return dirParts.slice(0, 2).join('/');
|
|
232
|
+
}
|
|
233
|
+
function groupFilesByDirectory(files, index) {
|
|
234
|
+
const groups = new Map();
|
|
235
|
+
for (const f of files) {
|
|
236
|
+
const key = groupKey(f.path);
|
|
237
|
+
let g = groups.get(key);
|
|
238
|
+
if (!g) {
|
|
239
|
+
g = { fileCount: 0, kinds: zeroSymbolsByKind() };
|
|
240
|
+
groups.set(key, g);
|
|
241
|
+
}
|
|
242
|
+
g.fileCount++;
|
|
243
|
+
for (const sym of index.getSymbolsInFile(f.path)) {
|
|
244
|
+
g.kinds[sym.kind]++;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return [...groups.entries()]
|
|
248
|
+
.sort((a, b) => b[1].fileCount - a[1].fileCount || a[0].localeCompare(b[0]))
|
|
249
|
+
.slice(0, MAX_DIR_GROUPS)
|
|
250
|
+
.map(([dir, g]) => ({
|
|
251
|
+
dir: dir === '(root)' ? '(root)' : `${dir}/`,
|
|
252
|
+
fileCount: g.fileCount,
|
|
253
|
+
topKinds: topKinds(g.kinds, MAX_KINDS_PER_GROUP),
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
function topKinds(kinds, limit) {
|
|
257
|
+
return sortedKindCounts(kinds)
|
|
258
|
+
.slice(0, limit)
|
|
259
|
+
.map(([kind, count]) => ({ kind, count }));
|
|
260
|
+
}
|
|
261
|
+
function formatSymbolKinds(kinds) {
|
|
262
|
+
return sortedKindCounts(kinds)
|
|
263
|
+
.map(([k, c]) => `${c} ${pluralKind(k, c)}`)
|
|
264
|
+
.join(', ');
|
|
265
|
+
}
|
|
266
|
+
const PY_MAIN_GUARD_RE = /^if\s+__name__\s*==\s*['"]__main__['"]/m;
|
|
267
|
+
const PY_FILE_SCAN_CAP = 100;
|
|
268
|
+
async function collectPythonMainGuards(pyFiles, projectRoot, slots) {
|
|
269
|
+
// Re-sort shallow-first here rather than trusting scan order: CodeIndex
|
|
270
|
+
// Map iteration order shifts as updateFile (remove + re-add) moves
|
|
271
|
+
// modified files to the end, so a root manage.py edited post-scan can
|
|
272
|
+
// land beyond PY_FILE_SCAN_CAP without this.
|
|
273
|
+
const sorted = [...pyFiles].sort(compareShallowFirst);
|
|
274
|
+
const limit = Math.min(sorted.length, PY_FILE_SCAN_CAP);
|
|
275
|
+
const reads = sorted.slice(0, limit).map(async (f) => {
|
|
276
|
+
try {
|
|
277
|
+
const content = await fs.readFile(join(projectRoot, f.path), 'utf8');
|
|
278
|
+
return PY_MAIN_GUARD_RE.test(content) ? f.path : null;
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
const code = err?.code;
|
|
282
|
+
if (code !== 'ENOENT' && code !== 'EISDIR') {
|
|
283
|
+
log.warn(`overview: failed to read ${f.path}: ${errMsg(err)}`);
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
const results = await Promise.all(reads);
|
|
289
|
+
const out = [];
|
|
290
|
+
for (const r of results) {
|
|
291
|
+
if (r === null)
|
|
292
|
+
continue;
|
|
293
|
+
if (out.length >= slots)
|
|
294
|
+
break;
|
|
295
|
+
out.push(r);
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
function fallbackByExportCount(allFiles, index, excluded, slots) {
|
|
300
|
+
const ranked = [];
|
|
301
|
+
for (const f of allFiles) {
|
|
302
|
+
if (excluded.has(f.path))
|
|
303
|
+
continue;
|
|
304
|
+
const count = index
|
|
305
|
+
.getSymbolsInFile(f.path)
|
|
306
|
+
.filter(isTopLevelExport).length;
|
|
307
|
+
if (count > 0)
|
|
308
|
+
ranked.push({ path: f.path, count });
|
|
309
|
+
}
|
|
310
|
+
ranked.sort((a, b) => b.count - a.count || a.path.localeCompare(b.path));
|
|
311
|
+
return ranked.slice(0, slots).map((r) => r.path);
|
|
312
|
+
}
|
|
313
|
+
function insertCandidates(candidates, paths, index) {
|
|
314
|
+
for (const p of paths) {
|
|
315
|
+
if (candidates.has(p))
|
|
316
|
+
continue;
|
|
317
|
+
if (candidates.size >= MAX_ENTRY_POINTS)
|
|
318
|
+
break;
|
|
319
|
+
candidates.set(p, makeEntry(p, index));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function collectEntryPoints(index, allFiles, projectRoot) {
|
|
323
|
+
const candidates = new Map();
|
|
324
|
+
// Restrict to parseable files so package.json self-export patterns like
|
|
325
|
+
// `"./package.json": "./package.json"` don't resolve and pollute the list.
|
|
326
|
+
const indexed = new Set();
|
|
327
|
+
// Split entry-name files: non-barrels (main.py, server.ts) take
|
|
328
|
+
// precedence over barrels (__init__.py, index.ts). Without the split,
|
|
329
|
+
// 15+ barrels in a monorepo saturate the cap and prevent the main-guard
|
|
330
|
+
// tier from ever surfacing manage.py-style scripts. Sort each sub-tier
|
|
331
|
+
// shallow-first; otherwise 15+ packages/*/server.ts crowds out src/server.ts.
|
|
332
|
+
const nonBarrelEntries = [];
|
|
333
|
+
const barrelEntries = [];
|
|
334
|
+
for (const f of allFiles) {
|
|
335
|
+
if (f.language !== LANGUAGE_UNKNOWN)
|
|
336
|
+
indexed.add(f.path);
|
|
337
|
+
const name = basename(f.path);
|
|
338
|
+
if (!ENTRY_POINT_FILENAME_RE.test(name))
|
|
339
|
+
continue;
|
|
340
|
+
if (BARREL_ENTRY_RE.test(name))
|
|
341
|
+
barrelEntries.push(f);
|
|
342
|
+
else
|
|
343
|
+
nonBarrelEntries.push(f);
|
|
344
|
+
}
|
|
345
|
+
nonBarrelEntries.sort(compareShallowFirst);
|
|
346
|
+
barrelEntries.sort(compareShallowFirst);
|
|
347
|
+
// package.json entries are inserted first so they survive cap-clipping
|
|
348
|
+
// in barrel-heavy monorepos.
|
|
349
|
+
const pkgPaths = await readPackageJsonEntries(projectRoot);
|
|
350
|
+
const resolvedPkg = pkgPaths
|
|
351
|
+
.map((p) => resolveIndexedPath(p, indexed))
|
|
352
|
+
.filter((r) => r !== undefined);
|
|
353
|
+
insertCandidates(candidates, resolvedPkg, index);
|
|
354
|
+
if (candidates.size < MAX_ENTRY_POINTS) {
|
|
355
|
+
insertCandidates(candidates, nonBarrelEntries.map((f) => f.path), index);
|
|
356
|
+
}
|
|
357
|
+
// Catches Python scripts (e.g. Django manage.py, train.py) whose names
|
|
358
|
+
// don't match ENTRY_POINT_FILENAME_RE but use the main-guard idiom.
|
|
359
|
+
if (candidates.size < MAX_ENTRY_POINTS) {
|
|
360
|
+
const pyCandidates = allFiles.filter((f) => f.language === 'python' && !candidates.has(f.path));
|
|
361
|
+
const guarded = await collectPythonMainGuards(pyCandidates, projectRoot, MAX_ENTRY_POINTS - candidates.size);
|
|
362
|
+
insertCandidates(candidates, guarded, index);
|
|
363
|
+
}
|
|
364
|
+
if (candidates.size < MAX_ENTRY_POINTS) {
|
|
365
|
+
insertCandidates(candidates, barrelEntries.map((f) => f.path), index);
|
|
366
|
+
}
|
|
367
|
+
// Fallback for library projects: rank remaining files by top-level
|
|
368
|
+
// export count so a public API in src/foo.ts surfaces even without a
|
|
369
|
+
// package.json main, entry-named file, or Python main guard.
|
|
370
|
+
if (candidates.size < MAX_ENTRY_POINTS) {
|
|
371
|
+
const fallback = fallbackByExportCount(allFiles, index, candidates, MAX_ENTRY_POINTS - candidates.size);
|
|
372
|
+
insertCandidates(candidates, fallback, index);
|
|
373
|
+
}
|
|
374
|
+
return [...candidates.values()].slice(0, MAX_ENTRY_POINTS);
|
|
375
|
+
}
|
|
376
|
+
function isTopLevelExport(s) {
|
|
377
|
+
return s.exported && !isClassMember(s);
|
|
378
|
+
}
|
|
379
|
+
function makeEntry(file, index) {
|
|
380
|
+
const exported = index.getSymbolsInFile(file).filter(isTopLevelExport);
|
|
381
|
+
if (exported.length === 0)
|
|
382
|
+
return { file };
|
|
383
|
+
if (exported.length === 1) {
|
|
384
|
+
const e = exported[0];
|
|
385
|
+
return { file, symbol: e.name, line: e.startLine };
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
file,
|
|
389
|
+
summary: `exports ${exported.length} ${plural('symbol', exported.length)}`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const RESOLVE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
393
|
+
function resolveIndexedPath(p, indexed) {
|
|
394
|
+
// "." and "" both denote the project root — collapse to empty prefix.
|
|
395
|
+
const cleaned = p === '.' || p === '' ? '' : p.replace(/\/$/, '');
|
|
396
|
+
if (indexed.has(cleaned))
|
|
397
|
+
return cleaned;
|
|
398
|
+
const bases = [cleaned, cleaned ? `${cleaned}/index` : 'index'];
|
|
399
|
+
for (const base of bases) {
|
|
400
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
401
|
+
const candidate = base + ext;
|
|
402
|
+
if (indexed.has(candidate))
|
|
403
|
+
return candidate;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
async function readPackageJsonEntries(projectRoot) {
|
|
409
|
+
const pkgPath = join(projectRoot, 'package.json');
|
|
410
|
+
let raw;
|
|
411
|
+
try {
|
|
412
|
+
raw = await fs.readFile(pkgPath, 'utf8');
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
const code = err?.code;
|
|
416
|
+
if (code !== 'ENOENT') {
|
|
417
|
+
log.warn(`overview: failed to read ${pkgPath}: ${errMsg(err)}`);
|
|
418
|
+
}
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
let parsed;
|
|
422
|
+
try {
|
|
423
|
+
parsed = JSON.parse(raw);
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
log.warn(`overview: failed to parse ${pkgPath}: ${errMsg(err)}`);
|
|
427
|
+
return [];
|
|
428
|
+
}
|
|
429
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
430
|
+
return [];
|
|
431
|
+
}
|
|
432
|
+
const pkg = parsed;
|
|
433
|
+
const out = [];
|
|
434
|
+
if (typeof pkg.main === 'string' && pkg.main.length > 0) {
|
|
435
|
+
out.push(normalizeRelative(pkg.main));
|
|
436
|
+
}
|
|
437
|
+
if (typeof pkg.bin === 'string' && pkg.bin.length > 0) {
|
|
438
|
+
out.push(normalizeRelative(pkg.bin));
|
|
439
|
+
}
|
|
440
|
+
else if (pkg.bin &&
|
|
441
|
+
typeof pkg.bin === 'object' &&
|
|
442
|
+
!Array.isArray(pkg.bin)) {
|
|
443
|
+
for (const v of Object.values(pkg.bin)) {
|
|
444
|
+
if (typeof v === 'string' && v.length > 0) {
|
|
445
|
+
out.push(normalizeRelative(v));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (pkg.exports !== undefined) {
|
|
450
|
+
collectExportPaths(pkg.exports, out);
|
|
451
|
+
}
|
|
452
|
+
return out;
|
|
453
|
+
}
|
|
454
|
+
function collectExportPaths(node, out) {
|
|
455
|
+
if (typeof node === 'string') {
|
|
456
|
+
// Skip wildcard subpath patterns ("./features/*" → "./src/features/*.js")
|
|
457
|
+
// since they require glob expansion against the file system.
|
|
458
|
+
if (node.startsWith('./') && !node.includes('*')) {
|
|
459
|
+
out.push(normalizeRelative(node));
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (Array.isArray(node)) {
|
|
464
|
+
for (const v of node)
|
|
465
|
+
collectExportPaths(v, out);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (typeof node === 'object' && node !== null) {
|
|
469
|
+
// Skip the `types` exports condition: it points to .d.ts metadata,
|
|
470
|
+
// not runtime entry points. Subpath keys always start with "." per
|
|
471
|
+
// the Node.js exports spec, so a bare "types" key is unambiguously
|
|
472
|
+
// the type-declaration condition.
|
|
473
|
+
for (const [k, v] of Object.entries(node)) {
|
|
474
|
+
if (k === 'types')
|
|
475
|
+
continue;
|
|
476
|
+
collectExportPaths(v, out);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function normalizeRelative(p) {
|
|
481
|
+
return p.replace(/\\/g, '/').replace(/^\.\/+/, '');
|
|
482
|
+
}
|