@kodus/kodus-graph 0.2.9 → 0.2.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/analysis/blast-radius.d.ts +1 -1
- package/dist/analysis/blast-radius.js +19 -21
- package/dist/analysis/context-builder.js +13 -4
- package/dist/analysis/diff.d.ts +6 -0
- package/dist/analysis/diff.js +16 -1
- package/dist/analysis/enrich.d.ts +1 -1
- package/dist/analysis/enrich.js +37 -9
- package/dist/analysis/prompt-formatter.js +8 -1
- package/dist/cli.js +2 -0
- package/dist/commands/analyze.js +5 -3
- package/dist/commands/diff.js +2 -2
- package/dist/commands/parse.d.ts +1 -0
- package/dist/commands/parse.js +3 -3
- package/dist/commands/update.js +2 -2
- package/dist/graph/builder.d.ts +5 -1
- package/dist/graph/builder.js +35 -3
- package/dist/graph/edges.d.ts +5 -1
- package/dist/graph/edges.js +61 -7
- package/dist/graph/types.d.ts +3 -0
- package/dist/parser/batch.d.ts +1 -0
- package/dist/parser/batch.js +18 -3
- package/dist/parser/languages.js +1 -0
- package/dist/resolver/external-detector.d.ts +11 -0
- package/dist/resolver/external-detector.js +820 -0
- package/dist/resolver/fs-cache.d.ts +8 -0
- package/dist/resolver/fs-cache.js +36 -0
- package/dist/resolver/import-resolver.js +130 -32
- package/dist/resolver/languages/csharp.d.ts +2 -0
- package/dist/resolver/languages/csharp.js +69 -6
- package/dist/resolver/languages/go.js +8 -7
- package/dist/resolver/languages/java.js +102 -17
- package/dist/resolver/languages/php.js +26 -5
- package/dist/resolver/languages/python.js +79 -3
- package/dist/resolver/languages/ruby.d.ts +16 -1
- package/dist/resolver/languages/ruby.js +58 -7
- package/dist/resolver/languages/rust.js +8 -7
- package/dist/resolver/languages/typescript.d.ts +8 -0
- package/dist/resolver/languages/typescript.js +193 -17
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared filesystem cache for resolvers.
|
|
3
|
+
* Caches existsSync and readdirSync results to avoid repeated disk I/O.
|
|
4
|
+
* Call clearFsCache() between analysis runs.
|
|
5
|
+
*/
|
|
6
|
+
export declare function cachedExists(path: string): boolean;
|
|
7
|
+
export declare function cachedReaddir(path: string): string[];
|
|
8
|
+
export declare function clearFsCache(): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared filesystem cache for resolvers.
|
|
3
|
+
* Caches existsSync and readdirSync results to avoid repeated disk I/O.
|
|
4
|
+
* Call clearFsCache() between analysis runs.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readdirSync } from 'fs';
|
|
7
|
+
const existsCache = new Map();
|
|
8
|
+
const readdirCache = new Map();
|
|
9
|
+
export function cachedExists(path) {
|
|
10
|
+
const cached = existsCache.get(path);
|
|
11
|
+
if (cached !== undefined) {
|
|
12
|
+
return cached;
|
|
13
|
+
}
|
|
14
|
+
const result = existsSync(path);
|
|
15
|
+
existsCache.set(path, result);
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
export function cachedReaddir(path) {
|
|
19
|
+
const cached = readdirCache.get(path);
|
|
20
|
+
if (cached !== undefined) {
|
|
21
|
+
return cached;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const result = readdirSync(path).sort();
|
|
25
|
+
readdirCache.set(path, result);
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
readdirCache.set(path, []);
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function clearFsCache() {
|
|
34
|
+
existsCache.clear();
|
|
35
|
+
readdirCache.clear();
|
|
36
|
+
}
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* Routes import resolution to language-specific resolvers and
|
|
5
5
|
* falls back to tsconfig aliases for TypeScript/JavaScript.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
8
8
|
import { join, resolve as resolvePath } from 'path';
|
|
9
9
|
import { log } from '../shared/logger';
|
|
10
10
|
import { ensureWithinRoot } from '../shared/safe-path';
|
|
11
|
+
import { detectExternal } from './external-detector';
|
|
12
|
+
import { cachedExists } from './fs-cache';
|
|
11
13
|
import { resolve as resolveCsImport } from './languages/csharp';
|
|
12
14
|
import { resolve as resolveGoImport } from './languages/go';
|
|
13
15
|
import { resolve as resolveJavaImport } from './languages/java';
|
|
@@ -15,7 +17,20 @@ import { resolve as resolvePhpImport } from './languages/php';
|
|
|
15
17
|
import { resolve as resolvePyImport } from './languages/python';
|
|
16
18
|
import { resolve as resolveRbImport } from './languages/ruby';
|
|
17
19
|
import { resolve as resolveRustImport } from './languages/rust';
|
|
18
|
-
import { loadTsconfigAliases, resolve as resolveTsImport, resolveWithAliases } from './languages/typescript';
|
|
20
|
+
import { loadBundlerAliases, loadTsconfigAliases, resolve as resolveTsImport, resolveWithAliases, } from './languages/typescript';
|
|
21
|
+
/**
|
|
22
|
+
* Registered import resolvers by language key.
|
|
23
|
+
*
|
|
24
|
+
* IMPORTANT: When adding a new language, you MUST:
|
|
25
|
+
* 1. Create a resolver in src/resolver/languages/<lang>.ts
|
|
26
|
+
* 2. Add it to this map
|
|
27
|
+
* 3. Add tests in tests/resolver/<lang>.test.ts
|
|
28
|
+
* 4. Add external detection in src/resolver/external-detector.ts
|
|
29
|
+
*
|
|
30
|
+
* If a language key from the parser is not in this map, resolveImport()
|
|
31
|
+
* will log a warning and return null. This is intentional — silent
|
|
32
|
+
* failures that default to another language's resolver cause wrong results.
|
|
33
|
+
*/
|
|
19
34
|
const RESOLVERS = {
|
|
20
35
|
ts: resolveTsImport,
|
|
21
36
|
javascript: resolveTsImport,
|
|
@@ -34,7 +49,7 @@ const RESOLVERS = {
|
|
|
34
49
|
*/
|
|
35
50
|
function resolveHashImport(modulePath, repoRoot) {
|
|
36
51
|
const pkgPath = join(repoRoot, 'package.json');
|
|
37
|
-
if (!
|
|
52
|
+
if (!cachedExists(pkgPath)) {
|
|
38
53
|
return null;
|
|
39
54
|
}
|
|
40
55
|
try {
|
|
@@ -50,7 +65,7 @@ function resolveHashImport(modulePath, repoRoot) {
|
|
|
50
65
|
if (pattern === modulePath) {
|
|
51
66
|
// Exact match: "#utils" -> "./src/shared/utils.ts"
|
|
52
67
|
const resolved = resolvePath(repoRoot, target);
|
|
53
|
-
if (
|
|
68
|
+
if (cachedExists(resolved)) {
|
|
54
69
|
return resolved;
|
|
55
70
|
}
|
|
56
71
|
}
|
|
@@ -60,7 +75,7 @@ function resolveHashImport(modulePath, repoRoot) {
|
|
|
60
75
|
if (modulePath.startsWith(prefix)) {
|
|
61
76
|
const rest = modulePath.slice(prefix.length); // "connection"
|
|
62
77
|
const resolved = resolvePath(repoRoot, target.replace('*', rest));
|
|
63
|
-
if (
|
|
78
|
+
if (cachedExists(resolved)) {
|
|
64
79
|
return resolved;
|
|
65
80
|
}
|
|
66
81
|
}
|
|
@@ -72,28 +87,59 @@ function resolveHashImport(modulePath, repoRoot) {
|
|
|
72
87
|
}
|
|
73
88
|
return null;
|
|
74
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a conditional export value to a single string path.
|
|
92
|
+
* When the value is a plain string, return it directly.
|
|
93
|
+
* When it's an object with condition keys, prefer: types > import > default > first value.
|
|
94
|
+
*/
|
|
95
|
+
function resolveExportValue(value) {
|
|
96
|
+
if (typeof value === 'string') {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
100
|
+
const obj = value;
|
|
101
|
+
for (const key of ['types', 'import', 'default']) {
|
|
102
|
+
if (typeof obj[key] === 'string') {
|
|
103
|
+
return obj[key];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Fallback: first value that is a string
|
|
107
|
+
for (const v of Object.values(obj)) {
|
|
108
|
+
if (typeof v === 'string') {
|
|
109
|
+
return v;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
75
115
|
/**
|
|
76
116
|
* Resolve monorepo workspace package exports.
|
|
77
117
|
* Scans workspace directories to find packages matching the import specifier.
|
|
78
118
|
*/
|
|
79
119
|
function resolveWorkspaceExport(modulePath, repoRoot) {
|
|
80
120
|
const rootPkgPath = join(repoRoot, 'package.json');
|
|
81
|
-
if (!
|
|
121
|
+
if (!cachedExists(rootPkgPath)) {
|
|
82
122
|
return null;
|
|
83
123
|
}
|
|
84
124
|
try {
|
|
85
125
|
const rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf-8'));
|
|
86
|
-
|
|
87
|
-
if (
|
|
126
|
+
let workspaceGlobs;
|
|
127
|
+
if (Array.isArray(rootPkg?.workspaces)) {
|
|
128
|
+
workspaceGlobs = rootPkg.workspaces;
|
|
129
|
+
}
|
|
130
|
+
else if (rootPkg?.workspaces?.packages && Array.isArray(rootPkg.workspaces.packages)) {
|
|
131
|
+
workspaceGlobs = rootPkg.workspaces.packages;
|
|
132
|
+
}
|
|
133
|
+
if (!workspaceGlobs) {
|
|
88
134
|
return null;
|
|
89
135
|
}
|
|
90
136
|
// Collect all workspace package directories
|
|
91
137
|
const pkgDirs = [];
|
|
92
|
-
for (const ws of
|
|
138
|
+
for (const ws of workspaceGlobs) {
|
|
93
139
|
if (ws.endsWith('/*')) {
|
|
94
140
|
// Glob pattern like "packages/*"
|
|
95
141
|
const parentDir = join(repoRoot, ws.slice(0, -2));
|
|
96
|
-
if (
|
|
142
|
+
if (cachedExists(parentDir)) {
|
|
97
143
|
const entries = readdirSync(parentDir, { withFileTypes: true });
|
|
98
144
|
for (const entry of entries) {
|
|
99
145
|
if (entry.isDirectory()) {
|
|
@@ -106,10 +152,10 @@ function resolveWorkspaceExport(modulePath, repoRoot) {
|
|
|
106
152
|
pkgDirs.push(join(repoRoot, ws));
|
|
107
153
|
}
|
|
108
154
|
}
|
|
109
|
-
// Search each workspace package for a matching name + exports
|
|
155
|
+
// Search each workspace package for a matching name + exports/main/module
|
|
110
156
|
for (const pkgDir of pkgDirs) {
|
|
111
157
|
const pkgJsonPath = join(pkgDir, 'package.json');
|
|
112
|
-
if (!
|
|
158
|
+
if (!cachedExists(pkgJsonPath)) {
|
|
113
159
|
continue;
|
|
114
160
|
}
|
|
115
161
|
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
@@ -118,28 +164,53 @@ function resolveWorkspaceExport(modulePath, repoRoot) {
|
|
|
118
164
|
continue;
|
|
119
165
|
}
|
|
120
166
|
const exports = pkg?.exports;
|
|
121
|
-
if (!exports || typeof exports !== 'object') {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
167
|
// Check if modulePath matches this package (exact or subpath)
|
|
125
168
|
if (modulePath === pkgName) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
169
|
+
if (exports && typeof exports === 'object') {
|
|
170
|
+
// Root export: "." entry
|
|
171
|
+
const target = resolveExportValue(exports['.']);
|
|
172
|
+
if (target) {
|
|
173
|
+
const resolved = resolvePath(pkgDir, target);
|
|
174
|
+
if (cachedExists(resolved)) {
|
|
175
|
+
return resolved;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else if (!exports) {
|
|
180
|
+
// Fallback to main or module fields
|
|
181
|
+
const fallback = pkg.main ?? pkg.module;
|
|
182
|
+
if (typeof fallback === 'string') {
|
|
183
|
+
const resolved = resolvePath(pkgDir, fallback);
|
|
184
|
+
if (cachedExists(resolved)) {
|
|
185
|
+
return resolved;
|
|
186
|
+
}
|
|
132
187
|
}
|
|
133
188
|
}
|
|
134
189
|
}
|
|
135
190
|
else if (modulePath.startsWith(`${pkgName}/`)) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
|
|
191
|
+
const subpath = modulePath.slice(pkgName.length + 1);
|
|
192
|
+
// 1. Try exports field first
|
|
193
|
+
if (exports && typeof exports === 'object') {
|
|
194
|
+
const exportKey = `./${subpath}`;
|
|
195
|
+
const target = resolveExportValue(exports[exportKey]);
|
|
196
|
+
if (target) {
|
|
197
|
+
const resolved = resolvePath(pkgDir, target);
|
|
198
|
+
if (cachedExists(resolved)) {
|
|
199
|
+
return resolved;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// 2. No exports or no match? Resolve subpath directly in package directory
|
|
204
|
+
const directBase = join(pkgDir, subpath);
|
|
205
|
+
for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
|
|
206
|
+
if (cachedExists(directBase + ext)) {
|
|
207
|
+
return resolvePath(directBase + ext);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
|
|
211
|
+
const idx = join(directBase, `index${ext}`);
|
|
212
|
+
if (cachedExists(idx)) {
|
|
213
|
+
return resolvePath(idx);
|
|
143
214
|
}
|
|
144
215
|
}
|
|
145
216
|
}
|
|
@@ -163,11 +234,21 @@ function resolveWorkspaceExport(modulePath, repoRoot) {
|
|
|
163
234
|
export function resolveImport(fromAbsFile, modulePath, lang, repoRoot, tsconfigAliases) {
|
|
164
235
|
const resolver = RESOLVERS[lang];
|
|
165
236
|
if (!resolver) {
|
|
237
|
+
log.warn('No import resolver registered for language', { lang, module: modulePath, from: fromAbsFile });
|
|
166
238
|
return null;
|
|
167
239
|
}
|
|
168
|
-
|
|
240
|
+
// Strip webpack/rollup loader syntax: !!loader1!loader2!actual/path
|
|
241
|
+
// The actual import path is always the last segment after the final '!'
|
|
242
|
+
if (modulePath.includes('!')) {
|
|
243
|
+
modulePath = modulePath.split('!').pop() || modulePath;
|
|
244
|
+
}
|
|
245
|
+
// TS/JS-specific fallbacks: tsconfig aliases, bundler aliases, #imports, workspace exports.
|
|
246
|
+
// These are Node.js/npm ecosystem features that don't apply to other languages.
|
|
247
|
+
// Other languages handle their own workspace/monorepo patterns inside their resolvers
|
|
248
|
+
// (Go: go.work, Rust: Cargo workspace, Java: Maven/Gradle modules).
|
|
249
|
+
const isTsOrJs = lang === 'ts' || lang === 'javascript' || lang === 'typescript';
|
|
169
250
|
// Handle package.json #imports (TS/JS only)
|
|
170
|
-
if (
|
|
251
|
+
if (isTsOrJs && modulePath.startsWith('#')) {
|
|
171
252
|
const result = resolveHashImport(modulePath, repoRoot);
|
|
172
253
|
if (result) {
|
|
173
254
|
try {
|
|
@@ -186,11 +267,18 @@ export function resolveImport(fromAbsFile, modulePath, lang, repoRoot, tsconfigA
|
|
|
186
267
|
}
|
|
187
268
|
let result = resolver(fromAbsFile, modulePath, repoRoot);
|
|
188
269
|
// Fallback: tsconfig aliases for TS/JS
|
|
189
|
-
if (!result &&
|
|
270
|
+
if (!result && isTsOrJs && tsconfigAliases?.size) {
|
|
190
271
|
result = resolveWithAliases(modulePath, tsconfigAliases, repoRoot);
|
|
191
272
|
}
|
|
273
|
+
// Fallback: bundler aliases (webpack/vite) for TS/JS bare specifiers
|
|
274
|
+
if (!result && isTsOrJs && !modulePath.startsWith('.')) {
|
|
275
|
+
const bundlerAliases = loadBundlerAliases(repoRoot);
|
|
276
|
+
if (bundlerAliases.size > 0) {
|
|
277
|
+
result = resolveWithAliases(modulePath, bundlerAliases, repoRoot);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
192
280
|
// Fallback: monorepo workspace exports for TS/JS bare specifiers
|
|
193
|
-
if (!result &&
|
|
281
|
+
if (!result && isTsOrJs && !modulePath.startsWith('.')) {
|
|
194
282
|
result = resolveWorkspaceExport(modulePath, repoRoot);
|
|
195
283
|
}
|
|
196
284
|
// Validate resolved path is within repo root
|
|
@@ -207,6 +295,16 @@ export function resolveImport(fromAbsFile, modulePath, lang, repoRoot, tsconfigA
|
|
|
207
295
|
return null;
|
|
208
296
|
}
|
|
209
297
|
}
|
|
298
|
+
// If still unresolved, check if it's an external package (for logging/debugging)
|
|
299
|
+
if (!result) {
|
|
300
|
+
const externalPkg = detectExternal(modulePath, lang, repoRoot);
|
|
301
|
+
if (externalPkg) {
|
|
302
|
+
// External package — expected null, don't log as warning
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
// Truly unresolved local import
|
|
306
|
+
log.debug('Unresolved local import', { from: fromAbsFile, module: modulePath });
|
|
307
|
+
}
|
|
210
308
|
return result;
|
|
211
309
|
}
|
|
212
310
|
export { loadTsconfigAliases };
|
|
@@ -1,18 +1,74 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join, resolve as resolvePath } from 'path';
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
2
|
+
import { dirname, join, resolve as resolvePath } from 'path';
|
|
3
|
+
import { cachedExists } from '../fs-cache';
|
|
3
4
|
const STDLIB_PREFIXES = ['System.', 'System', 'Microsoft.', 'Newtonsoft.'];
|
|
5
|
+
const slnProjectsCache = new Map();
|
|
6
|
+
/** Clear cached .sln data. Call between analysis runs or when switching repos. */
|
|
7
|
+
export function clearCache() {
|
|
8
|
+
slnProjectsCache.clear();
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parse .sln files at repoRoot to discover project directories.
|
|
12
|
+
* Lines like: Project("{FAE04EC0}") = "Name", "path/to/Project.csproj", "{GUID}"
|
|
13
|
+
*/
|
|
14
|
+
function getSlnProjectDirs(repoRoot) {
|
|
15
|
+
const cached = slnProjectsCache.get(repoRoot);
|
|
16
|
+
if (cached) {
|
|
17
|
+
return cached;
|
|
18
|
+
}
|
|
19
|
+
const dirs = [];
|
|
20
|
+
try {
|
|
21
|
+
const entries = readdirSync(repoRoot);
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (entry.endsWith('.sln')) {
|
|
24
|
+
const slnPath = join(repoRoot, entry);
|
|
25
|
+
try {
|
|
26
|
+
const content = readFileSync(slnPath, 'utf-8');
|
|
27
|
+
const projectRe = /^Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/gm;
|
|
28
|
+
let m = projectRe.exec(content);
|
|
29
|
+
while (m !== null) {
|
|
30
|
+
const csprojRelPath = m[1].replace(/\\/g, '/');
|
|
31
|
+
const projectDir = dirname(join(repoRoot, csprojRelPath));
|
|
32
|
+
if (cachedExists(projectDir)) {
|
|
33
|
+
dirs.push(projectDir);
|
|
34
|
+
}
|
|
35
|
+
m = projectRe.exec(content);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* ignore unreadable sln */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* ignore */
|
|
46
|
+
}
|
|
47
|
+
slnProjectsCache.set(repoRoot, dirs);
|
|
48
|
+
return dirs;
|
|
49
|
+
}
|
|
4
50
|
export function resolve(_fromAbsFile, modulePath, repoRoot) {
|
|
5
51
|
if (STDLIB_PREFIXES.some((p) => modulePath.startsWith(p))) {
|
|
6
52
|
return null;
|
|
7
53
|
}
|
|
8
54
|
const segments = modulePath.split('.');
|
|
55
|
+
// Collect search base directories: standard ones + .sln-discovered project dirs
|
|
56
|
+
const standardBases = ['', 'src', 'lib', 'Source'];
|
|
57
|
+
const slnDirs = getSlnProjectDirs(repoRoot);
|
|
9
58
|
// Try resolving as a .cs file first
|
|
10
59
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
11
60
|
const pathPart = segments.slice(i).join('/');
|
|
12
61
|
const candidate = `${pathPart}.cs`;
|
|
13
|
-
for (const base of
|
|
62
|
+
for (const base of standardBases) {
|
|
14
63
|
const full = join(repoRoot, base, candidate);
|
|
15
|
-
if (
|
|
64
|
+
if (cachedExists(full)) {
|
|
65
|
+
return resolvePath(full);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Also search in .sln-discovered project directories
|
|
69
|
+
for (const projDir of slnDirs) {
|
|
70
|
+
const full = join(projDir, candidate);
|
|
71
|
+
if (cachedExists(full)) {
|
|
16
72
|
return resolvePath(full);
|
|
17
73
|
}
|
|
18
74
|
}
|
|
@@ -20,9 +76,16 @@ export function resolve(_fromAbsFile, modulePath, repoRoot) {
|
|
|
20
76
|
// Try resolving as a directory (namespace → folder mapping)
|
|
21
77
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
22
78
|
const pathPart = segments.slice(i).join('/');
|
|
23
|
-
for (const base of
|
|
79
|
+
for (const base of standardBases) {
|
|
24
80
|
const full = join(repoRoot, base, pathPart);
|
|
25
|
-
if (
|
|
81
|
+
if (cachedExists(full) && statSync(full).isDirectory()) {
|
|
82
|
+
return resolvePath(full);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Also search in .sln-discovered project directories
|
|
86
|
+
for (const projDir of slnDirs) {
|
|
87
|
+
const full = join(projDir, pathPart);
|
|
88
|
+
if (cachedExists(full) && statSync(full).isDirectory()) {
|
|
26
89
|
return resolvePath(full);
|
|
27
90
|
}
|
|
28
91
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
2
|
import { join, resolve as resolvePath } from 'path';
|
|
3
|
+
import { cachedExists, cachedReaddir } from '../fs-cache';
|
|
3
4
|
const moduleCache = new Map();
|
|
4
5
|
const replaceCache = new Map();
|
|
5
6
|
const workspaceCache = new Map();
|
|
@@ -15,7 +16,7 @@ function getModuleName(repoRoot) {
|
|
|
15
16
|
return cached || null;
|
|
16
17
|
}
|
|
17
18
|
const goModPath = join(repoRoot, 'go.mod');
|
|
18
|
-
if (!
|
|
19
|
+
if (!cachedExists(goModPath)) {
|
|
19
20
|
moduleCache.set(repoRoot, '');
|
|
20
21
|
return null;
|
|
21
22
|
}
|
|
@@ -41,7 +42,7 @@ function getReplaceMap(repoRoot) {
|
|
|
41
42
|
}
|
|
42
43
|
const result = new Map();
|
|
43
44
|
const goModPath = join(repoRoot, 'go.mod');
|
|
44
|
-
if (!
|
|
45
|
+
if (!cachedExists(goModPath)) {
|
|
45
46
|
replaceCache.set(repoRoot, result);
|
|
46
47
|
return result;
|
|
47
48
|
}
|
|
@@ -73,7 +74,7 @@ function getWorkspaceModules(repoRoot) {
|
|
|
73
74
|
}
|
|
74
75
|
const result = new Map();
|
|
75
76
|
const goWorkPath = join(repoRoot, 'go.work');
|
|
76
|
-
if (!
|
|
77
|
+
if (!cachedExists(goWorkPath)) {
|
|
77
78
|
workspaceCache.set(repoRoot, result);
|
|
78
79
|
return result;
|
|
79
80
|
}
|
|
@@ -125,9 +126,9 @@ function isStdlib(modulePath) {
|
|
|
125
126
|
}
|
|
126
127
|
/** Find the first .go file (non-test) in a directory, or check for a .go file at the path. */
|
|
127
128
|
function findGoFile(absDir) {
|
|
128
|
-
if (
|
|
129
|
+
if (cachedExists(absDir)) {
|
|
129
130
|
try {
|
|
130
|
-
const files =
|
|
131
|
+
const files = cachedReaddir(absDir).sort();
|
|
131
132
|
const goFile = files.find((f) => f.endsWith('.go') && !f.endsWith('_test.go'));
|
|
132
133
|
if (goFile) {
|
|
133
134
|
return resolvePath(join(absDir, goFile));
|
|
@@ -137,7 +138,7 @@ function findGoFile(absDir) {
|
|
|
137
138
|
/* not a directory */
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
|
-
if (
|
|
141
|
+
if (cachedExists(`${absDir}.go`)) {
|
|
141
142
|
return resolvePath(`${absDir}.go`);
|
|
142
143
|
}
|
|
143
144
|
return null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
2
|
import { join, resolve as resolvePath } from 'path';
|
|
3
|
+
import { cachedExists, cachedReaddir } from '../fs-cache';
|
|
3
4
|
const STDLIB_PREFIXES = ['java.', 'javax.', 'sun.', 'com.sun.', 'jdk.'];
|
|
4
5
|
const SOURCE_ROOTS = ['src/main/java', 'src/main/kotlin', 'src', ''];
|
|
5
6
|
const EXTENSIONS = ['.java', '.kt'];
|
|
@@ -11,7 +12,7 @@ function collectSourceRoots(repoRoot) {
|
|
|
11
12
|
// Discover Gradle subprojects from settings.gradle / settings.gradle.kts
|
|
12
13
|
for (const settingsFile of ['settings.gradle', 'settings.gradle.kts']) {
|
|
13
14
|
const settingsPath = join(repoRoot, settingsFile);
|
|
14
|
-
if (!
|
|
15
|
+
if (!cachedExists(settingsPath)) {
|
|
15
16
|
continue;
|
|
16
17
|
}
|
|
17
18
|
const content = readFileSync(settingsPath, 'utf-8');
|
|
@@ -29,24 +30,69 @@ function collectSourceRoots(repoRoot) {
|
|
|
29
30
|
}
|
|
30
31
|
break; // only read first settings file found
|
|
31
32
|
}
|
|
32
|
-
// Discover
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
// Discover custom sourceSets from build.gradle / build.gradle.kts in subprojects
|
|
34
|
+
const gradleFiles = [];
|
|
35
|
+
// Collect build.gradle files: root + discovered subproject dirs
|
|
36
|
+
for (const buildFile of ['build.gradle', 'build.gradle.kts']) {
|
|
37
|
+
if (cachedExists(join(repoRoot, buildFile))) {
|
|
38
|
+
gradleFiles.push({ dir: '', file: buildFile });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Also check subproject directories already discovered above
|
|
42
|
+
const subDirs = new Set();
|
|
43
|
+
for (const r of roots) {
|
|
44
|
+
// Extract the subproject directory (everything before src/...)
|
|
45
|
+
const srcIdx = r.indexOf('/src');
|
|
46
|
+
if (srcIdx > 0) {
|
|
47
|
+
subDirs.add(r.slice(0, srcIdx));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const sub of subDirs) {
|
|
51
|
+
for (const buildFile of ['build.gradle', 'build.gradle.kts']) {
|
|
52
|
+
const buildPath = join(repoRoot, sub, buildFile);
|
|
53
|
+
if (cachedExists(buildPath)) {
|
|
54
|
+
gradleFiles.push({ dir: sub, file: join(sub, buildFile) });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const { dir, file } of gradleFiles) {
|
|
35
59
|
try {
|
|
36
|
-
const content = readFileSync(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
const content = readFileSync(join(repoRoot, file), 'utf-8');
|
|
61
|
+
// Match: srcDirs = ['path1', 'path2']
|
|
62
|
+
const srcDirsArrayRegex = /srcDirs\s*=\s*\[([^\]]+)\]/g;
|
|
63
|
+
let sdMatch = srcDirsArrayRegex.exec(content);
|
|
64
|
+
while (sdMatch !== null) {
|
|
65
|
+
const entries = sdMatch[1];
|
|
66
|
+
const pathRegex = /['"]([^'"]+)['"]/g;
|
|
67
|
+
let pathMatch = pathRegex.exec(entries);
|
|
68
|
+
while (pathMatch !== null) {
|
|
69
|
+
const srcDir = pathMatch[1];
|
|
70
|
+
const root = dir ? join(dir, srcDir) : srcDir;
|
|
71
|
+
if (!roots.includes(root)) {
|
|
72
|
+
roots.push(root);
|
|
73
|
+
}
|
|
74
|
+
pathMatch = pathRegex.exec(entries);
|
|
75
|
+
}
|
|
76
|
+
sdMatch = srcDirsArrayRegex.exec(content);
|
|
77
|
+
}
|
|
78
|
+
// Match: srcDir 'path' or srcDir "path"
|
|
79
|
+
const srcDirSingleRegex = /srcDir\s+['"]([^'"]+)['"]/g;
|
|
80
|
+
let singleMatch = srcDirSingleRegex.exec(content);
|
|
81
|
+
while (singleMatch !== null) {
|
|
82
|
+
const srcDir = singleMatch[1];
|
|
83
|
+
const root = dir ? join(dir, srcDir) : srcDir;
|
|
84
|
+
if (!roots.includes(root)) {
|
|
85
|
+
roots.push(root);
|
|
86
|
+
}
|
|
87
|
+
singleMatch = srcDirSingleRegex.exec(content);
|
|
44
88
|
}
|
|
45
89
|
}
|
|
46
90
|
catch {
|
|
47
|
-
//
|
|
91
|
+
// build.gradle read failed, continue
|
|
48
92
|
}
|
|
49
93
|
}
|
|
94
|
+
// Discover Maven subprojects from pom.xml (recursive)
|
|
95
|
+
discoverMavenModules(repoRoot, '', roots, 0);
|
|
50
96
|
return roots;
|
|
51
97
|
}
|
|
52
98
|
/**
|
|
@@ -57,13 +103,52 @@ function findFile(repoRoot, relPathNoExt, sourceRoots) {
|
|
|
57
103
|
for (const srcRoot of sourceRoots) {
|
|
58
104
|
for (const ext of EXTENSIONS) {
|
|
59
105
|
const candidate = join(repoRoot, srcRoot, relPathNoExt + ext);
|
|
60
|
-
if (
|
|
106
|
+
if (cachedExists(candidate)) {
|
|
61
107
|
return resolvePath(candidate);
|
|
62
108
|
}
|
|
63
109
|
}
|
|
64
110
|
}
|
|
65
111
|
return null;
|
|
66
112
|
}
|
|
113
|
+
const MAX_MAVEN_DEPTH = 5;
|
|
114
|
+
/**
|
|
115
|
+
* Recursively discover Maven modules from pom.xml files.
|
|
116
|
+
* Each module's pom.xml may declare its own <module> elements,
|
|
117
|
+
* forming a tree (e.g. Keycloak: root → services → sub-service).
|
|
118
|
+
*/
|
|
119
|
+
function discoverMavenModules(repoRoot, relDir, roots, depth) {
|
|
120
|
+
if (depth > MAX_MAVEN_DEPTH) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const pomPath = join(repoRoot, relDir, 'pom.xml');
|
|
124
|
+
if (!cachedExists(pomPath)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const content = readFileSync(pomPath, 'utf-8');
|
|
129
|
+
const moduleRegex = /<module>([^<]+)<\/module>/g;
|
|
130
|
+
let mvnMatch = moduleRegex.exec(content);
|
|
131
|
+
while (mvnMatch !== null) {
|
|
132
|
+
const moduleName = mvnMatch[1];
|
|
133
|
+
const moduleDir = relDir ? join(relDir, moduleName) : moduleName;
|
|
134
|
+
// Add source roots for this module
|
|
135
|
+
const javaRoot = join(moduleDir, 'src/main/java');
|
|
136
|
+
if (!roots.includes(javaRoot)) {
|
|
137
|
+
roots.push(javaRoot);
|
|
138
|
+
}
|
|
139
|
+
const kotlinRoot = join(moduleDir, 'src/main/kotlin');
|
|
140
|
+
if (!roots.includes(kotlinRoot)) {
|
|
141
|
+
roots.push(kotlinRoot);
|
|
142
|
+
}
|
|
143
|
+
// Recurse into the module's own pom.xml
|
|
144
|
+
discoverMavenModules(repoRoot, moduleDir, roots, depth + 1);
|
|
145
|
+
mvnMatch = moduleRegex.exec(content);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// pom.xml read failed, continue
|
|
150
|
+
}
|
|
151
|
+
}
|
|
67
152
|
export function resolve(_fromAbsFile, modulePath, repoRoot) {
|
|
68
153
|
if (STDLIB_PREFIXES.some((p) => modulePath.startsWith(p))) {
|
|
69
154
|
return null;
|
|
@@ -74,9 +159,9 @@ export function resolve(_fromAbsFile, modulePath, repoRoot) {
|
|
|
74
159
|
const packagePath = modulePath.slice(0, -2).replace(/\./g, '/');
|
|
75
160
|
for (const srcRoot of sourceRoots) {
|
|
76
161
|
const dirPath = join(repoRoot, srcRoot, packagePath);
|
|
77
|
-
if (
|
|
162
|
+
if (cachedExists(dirPath)) {
|
|
78
163
|
try {
|
|
79
|
-
const files =
|
|
164
|
+
const files = cachedReaddir(dirPath).filter((f) => EXTENSIONS.some((ext) => f.endsWith(ext)));
|
|
80
165
|
if (files.length > 0) {
|
|
81
166
|
return resolvePath(join(dirPath, files[0]));
|
|
82
167
|
}
|