@kodus/kodus-graph 0.2.8 → 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/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/analysis/blast-radius.d.ts +2 -0
- package/dist/analysis/blast-radius.js +55 -0
- package/dist/analysis/communities.d.ts +28 -0
- package/dist/analysis/communities.js +100 -0
- package/dist/analysis/context-builder.d.ts +34 -0
- package/dist/analysis/context-builder.js +92 -0
- package/dist/analysis/diff.d.ts +41 -0
- package/dist/analysis/diff.js +155 -0
- package/dist/analysis/enrich.d.ts +5 -0
- package/dist/analysis/enrich.js +126 -0
- package/dist/analysis/flows.d.ts +27 -0
- package/dist/analysis/flows.js +86 -0
- package/dist/analysis/inheritance.d.ts +3 -0
- package/dist/analysis/inheritance.js +31 -0
- package/dist/analysis/prompt-formatter.d.ts +2 -0
- package/dist/analysis/prompt-formatter.js +173 -0
- package/dist/analysis/risk-score.d.ts +4 -0
- package/dist/analysis/risk-score.js +51 -0
- package/dist/analysis/search.d.ts +11 -0
- package/dist/analysis/search.js +64 -0
- package/dist/analysis/test-gaps.d.ts +2 -0
- package/dist/analysis/test-gaps.js +14 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +210 -0
- package/dist/commands/analyze.d.ts +9 -0
- package/dist/commands/analyze.js +116 -0
- package/dist/commands/communities.d.ts +8 -0
- package/dist/commands/communities.js +9 -0
- package/dist/commands/context.d.ts +12 -0
- package/dist/commands/context.js +130 -0
- package/dist/commands/diff.d.ts +9 -0
- package/dist/commands/diff.js +89 -0
- package/dist/commands/flows.d.ts +8 -0
- package/dist/commands/flows.js +9 -0
- package/dist/commands/parse.d.ts +11 -0
- package/dist/commands/parse.js +101 -0
- package/dist/commands/search.d.ts +12 -0
- package/dist/commands/search.js +27 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.js +154 -0
- package/dist/graph/builder.d.ts +6 -0
- package/dist/graph/builder.js +248 -0
- package/dist/graph/edges.d.ts +23 -0
- package/dist/graph/edges.js +159 -0
- package/dist/graph/json-writer.d.ts +9 -0
- package/dist/graph/json-writer.js +38 -0
- package/dist/graph/loader.d.ts +13 -0
- package/dist/graph/loader.js +101 -0
- package/dist/graph/merger.d.ts +7 -0
- package/dist/graph/merger.js +18 -0
- package/dist/graph/types.d.ts +252 -0
- package/dist/graph/types.js +1 -0
- package/dist/parser/batch.d.ts +5 -0
- package/dist/parser/batch.js +93 -0
- package/dist/parser/discovery.d.ts +7 -0
- package/dist/parser/discovery.js +61 -0
- package/dist/parser/extractor.d.ts +4 -0
- package/dist/parser/extractor.js +33 -0
- package/dist/parser/extractors/generic.d.ts +8 -0
- package/dist/parser/extractors/generic.js +471 -0
- package/dist/parser/extractors/python.d.ts +8 -0
- package/dist/parser/extractors/python.js +133 -0
- package/dist/parser/extractors/ruby.d.ts +8 -0
- package/dist/parser/extractors/ruby.js +153 -0
- package/dist/parser/extractors/typescript.d.ts +10 -0
- package/dist/parser/extractors/typescript.js +365 -0
- package/dist/parser/languages.d.ts +32 -0
- package/dist/parser/languages.js +304 -0
- package/dist/resolver/call-resolver.d.ts +36 -0
- package/dist/resolver/call-resolver.js +178 -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-map.d.ts +12 -0
- package/dist/resolver/import-map.js +21 -0
- package/dist/resolver/import-resolver.d.ts +19 -0
- package/dist/resolver/import-resolver.js +310 -0
- package/dist/resolver/languages/csharp.d.ts +3 -0
- package/dist/resolver/languages/csharp.js +94 -0
- package/dist/resolver/languages/go.d.ts +3 -0
- package/dist/resolver/languages/go.js +197 -0
- package/dist/resolver/languages/java.d.ts +1 -0
- package/dist/resolver/languages/java.js +193 -0
- package/dist/resolver/languages/php.d.ts +3 -0
- package/dist/resolver/languages/php.js +75 -0
- package/dist/resolver/languages/python.d.ts +11 -0
- package/dist/resolver/languages/python.js +127 -0
- package/dist/resolver/languages/ruby.d.ts +24 -0
- package/dist/resolver/languages/ruby.js +110 -0
- package/dist/resolver/languages/rust.d.ts +1 -0
- package/dist/resolver/languages/rust.js +197 -0
- package/dist/resolver/languages/typescript.d.ts +35 -0
- package/dist/resolver/languages/typescript.js +416 -0
- package/dist/resolver/re-export-resolver.d.ts +24 -0
- package/dist/resolver/re-export-resolver.js +57 -0
- package/dist/resolver/symbol-table.d.ts +17 -0
- package/dist/resolver/symbol-table.js +60 -0
- package/dist/shared/extract-calls.d.ts +26 -0
- package/dist/shared/extract-calls.js +57 -0
- package/dist/shared/file-hash.d.ts +3 -0
- package/dist/shared/file-hash.js +10 -0
- package/dist/shared/filters.d.ts +3 -0
- package/dist/shared/filters.js +240 -0
- package/dist/shared/logger.d.ts +6 -0
- package/dist/shared/logger.js +17 -0
- package/dist/shared/qualified-name.d.ts +1 -0
- package/dist/shared/qualified-name.js +9 -0
- package/dist/shared/safe-path.d.ts +6 -0
- package/dist/shared/safe-path.js +29 -0
- package/dist/shared/schemas.d.ts +43 -0
- package/dist/shared/schemas.js +30 -0
- package/dist/shared/temp.d.ts +11 -0
- package/{src/shared/temp.ts → dist/shared/temp.js} +4 -5
- package/package.json +20 -6
- package/src/analysis/blast-radius.ts +0 -54
- package/src/analysis/communities.ts +0 -135
- package/src/analysis/context-builder.ts +0 -130
- package/src/analysis/diff.ts +0 -169
- package/src/analysis/enrich.ts +0 -110
- package/src/analysis/flows.ts +0 -112
- package/src/analysis/inheritance.ts +0 -34
- package/src/analysis/prompt-formatter.ts +0 -175
- package/src/analysis/risk-score.ts +0 -62
- package/src/analysis/search.ts +0 -76
- package/src/analysis/test-gaps.ts +0 -21
- package/src/cli.ts +0 -210
- package/src/commands/analyze.ts +0 -128
- package/src/commands/communities.ts +0 -19
- package/src/commands/context.ts +0 -182
- package/src/commands/diff.ts +0 -96
- package/src/commands/flows.ts +0 -19
- package/src/commands/parse.ts +0 -124
- package/src/commands/search.ts +0 -41
- package/src/commands/update.ts +0 -166
- package/src/graph/builder.ts +0 -209
- package/src/graph/edges.ts +0 -101
- package/src/graph/json-writer.ts +0 -43
- package/src/graph/loader.ts +0 -113
- package/src/graph/merger.ts +0 -25
- package/src/graph/types.ts +0 -283
- package/src/parser/batch.ts +0 -82
- package/src/parser/discovery.ts +0 -75
- package/src/parser/extractor.ts +0 -37
- package/src/parser/extractors/generic.ts +0 -132
- package/src/parser/extractors/python.ts +0 -133
- package/src/parser/extractors/ruby.ts +0 -147
- package/src/parser/extractors/typescript.ts +0 -350
- package/src/parser/languages.ts +0 -122
- package/src/resolver/call-resolver.ts +0 -244
- package/src/resolver/import-map.ts +0 -27
- package/src/resolver/import-resolver.ts +0 -72
- package/src/resolver/languages/csharp.ts +0 -7
- package/src/resolver/languages/go.ts +0 -7
- package/src/resolver/languages/java.ts +0 -7
- package/src/resolver/languages/php.ts +0 -7
- package/src/resolver/languages/python.ts +0 -35
- package/src/resolver/languages/ruby.ts +0 -21
- package/src/resolver/languages/rust.ts +0 -7
- package/src/resolver/languages/typescript.ts +0 -168
- package/src/resolver/re-export-resolver.ts +0 -66
- package/src/resolver/symbol-table.ts +0 -67
- package/src/shared/extract-calls.ts +0 -75
- package/src/shared/file-hash.ts +0 -12
- package/src/shared/filters.ts +0 -243
- package/src/shared/logger.ts +0 -17
- package/src/shared/qualified-name.ts +0 -5
- package/src/shared/safe-path.ts +0 -31
- package/src/shared/schemas.ts +0 -32
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import map: tracks which symbols are imported from where per file.
|
|
3
|
+
*
|
|
4
|
+
* For each importing file, maps symbol names to the resolved file path
|
|
5
|
+
* they were imported from. Used by the call resolver to connect
|
|
6
|
+
* function calls to their definitions across files.
|
|
7
|
+
*/
|
|
8
|
+
export interface ImportMap {
|
|
9
|
+
add(file: string, name: string, targetFile: string): void;
|
|
10
|
+
lookup(file: string, name: string): string | null;
|
|
11
|
+
}
|
|
12
|
+
export declare function createImportMap(): ImportMap;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import map: tracks which symbols are imported from where per file.
|
|
3
|
+
*
|
|
4
|
+
* For each importing file, maps symbol names to the resolved file path
|
|
5
|
+
* they were imported from. Used by the call resolver to connect
|
|
6
|
+
* function calls to their definitions across files.
|
|
7
|
+
*/
|
|
8
|
+
export function createImportMap() {
|
|
9
|
+
const map = new Map();
|
|
10
|
+
return {
|
|
11
|
+
add(file, name, targetFile) {
|
|
12
|
+
if (!map.has(file)) {
|
|
13
|
+
map.set(file, new Map());
|
|
14
|
+
}
|
|
15
|
+
map.get(file).set(name, targetFile);
|
|
16
|
+
},
|
|
17
|
+
lookup(file, name) {
|
|
18
|
+
return map.get(file)?.get(name) ?? null;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import resolver dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Routes import resolution to language-specific resolvers and
|
|
5
|
+
* falls back to tsconfig aliases for TypeScript/JavaScript.
|
|
6
|
+
*/
|
|
7
|
+
import { loadTsconfigAliases } from './languages/typescript';
|
|
8
|
+
/**
|
|
9
|
+
* Resolve an import from one file to another.
|
|
10
|
+
*
|
|
11
|
+
* @param fromAbsFile - Absolute path of the importing file
|
|
12
|
+
* @param modulePath - The import specifier (e.g., './auth', 'express', '@/lib/db')
|
|
13
|
+
* @param lang - Language key (ts, javascript, typescript, python, ruby, etc.)
|
|
14
|
+
* @param repoRoot - Absolute path to the repository root
|
|
15
|
+
* @param tsconfigAliases - Optional pre-loaded tsconfig aliases for TS/JS
|
|
16
|
+
* @returns Absolute path to the resolved file, or null if unresolvable
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveImport(fromAbsFile: string, modulePath: string, lang: string, repoRoot: string, tsconfigAliases?: Map<string, string[]>): string | null;
|
|
19
|
+
export { loadTsconfigAliases };
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import resolver dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Routes import resolution to language-specific resolvers and
|
|
5
|
+
* falls back to tsconfig aliases for TypeScript/JavaScript.
|
|
6
|
+
*/
|
|
7
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
8
|
+
import { join, resolve as resolvePath } from 'path';
|
|
9
|
+
import { log } from '../shared/logger';
|
|
10
|
+
import { ensureWithinRoot } from '../shared/safe-path';
|
|
11
|
+
import { detectExternal } from './external-detector';
|
|
12
|
+
import { cachedExists } from './fs-cache';
|
|
13
|
+
import { resolve as resolveCsImport } from './languages/csharp';
|
|
14
|
+
import { resolve as resolveGoImport } from './languages/go';
|
|
15
|
+
import { resolve as resolveJavaImport } from './languages/java';
|
|
16
|
+
import { resolve as resolvePhpImport } from './languages/php';
|
|
17
|
+
import { resolve as resolvePyImport } from './languages/python';
|
|
18
|
+
import { resolve as resolveRbImport } from './languages/ruby';
|
|
19
|
+
import { resolve as resolveRustImport } from './languages/rust';
|
|
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
|
+
*/
|
|
34
|
+
const RESOLVERS = {
|
|
35
|
+
ts: resolveTsImport,
|
|
36
|
+
javascript: resolveTsImport,
|
|
37
|
+
typescript: resolveTsImport,
|
|
38
|
+
python: resolvePyImport,
|
|
39
|
+
ruby: resolveRbImport,
|
|
40
|
+
go: resolveGoImport,
|
|
41
|
+
java: resolveJavaImport,
|
|
42
|
+
rust: resolveRustImport,
|
|
43
|
+
csharp: resolveCsImport,
|
|
44
|
+
php: resolvePhpImport,
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Resolve package.json #imports (Node.js subpath imports).
|
|
48
|
+
* Handles both exact matches and wildcard patterns.
|
|
49
|
+
*/
|
|
50
|
+
function resolveHashImport(modulePath, repoRoot) {
|
|
51
|
+
const pkgPath = join(repoRoot, 'package.json');
|
|
52
|
+
if (!cachedExists(pkgPath)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
57
|
+
const imports = pkg?.imports;
|
|
58
|
+
if (!imports) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
for (const [pattern, target] of Object.entries(imports)) {
|
|
62
|
+
if (typeof target !== 'string') {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (pattern === modulePath) {
|
|
66
|
+
// Exact match: "#utils" -> "./src/shared/utils.ts"
|
|
67
|
+
const resolved = resolvePath(repoRoot, target);
|
|
68
|
+
if (cachedExists(resolved)) {
|
|
69
|
+
return resolved;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Wildcard match: "#db/*" -> "./src/db/*.ts"
|
|
73
|
+
if (pattern.includes('*')) {
|
|
74
|
+
const prefix = pattern.split('*')[0]; // "#db/"
|
|
75
|
+
if (modulePath.startsWith(prefix)) {
|
|
76
|
+
const rest = modulePath.slice(prefix.length); // "connection"
|
|
77
|
+
const resolved = resolvePath(repoRoot, target.replace('*', rest));
|
|
78
|
+
if (cachedExists(resolved)) {
|
|
79
|
+
return resolved;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// ignore parse errors
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
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
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolve monorepo workspace package exports.
|
|
117
|
+
* Scans workspace directories to find packages matching the import specifier.
|
|
118
|
+
*/
|
|
119
|
+
function resolveWorkspaceExport(modulePath, repoRoot) {
|
|
120
|
+
const rootPkgPath = join(repoRoot, 'package.json');
|
|
121
|
+
if (!cachedExists(rootPkgPath)) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf-8'));
|
|
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) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
// Collect all workspace package directories
|
|
137
|
+
const pkgDirs = [];
|
|
138
|
+
for (const ws of workspaceGlobs) {
|
|
139
|
+
if (ws.endsWith('/*')) {
|
|
140
|
+
// Glob pattern like "packages/*"
|
|
141
|
+
const parentDir = join(repoRoot, ws.slice(0, -2));
|
|
142
|
+
if (cachedExists(parentDir)) {
|
|
143
|
+
const entries = readdirSync(parentDir, { withFileTypes: true });
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
if (entry.isDirectory()) {
|
|
146
|
+
pkgDirs.push(join(parentDir, entry.name));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
pkgDirs.push(join(repoRoot, ws));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Search each workspace package for a matching name + exports/main/module
|
|
156
|
+
for (const pkgDir of pkgDirs) {
|
|
157
|
+
const pkgJsonPath = join(pkgDir, 'package.json');
|
|
158
|
+
if (!cachedExists(pkgJsonPath)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
162
|
+
const pkgName = pkg?.name;
|
|
163
|
+
if (!pkgName) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const exports = pkg?.exports;
|
|
167
|
+
// Check if modulePath matches this package (exact or subpath)
|
|
168
|
+
if (modulePath === pkgName) {
|
|
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
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (modulePath.startsWith(`${pkgName}/`)) {
|
|
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);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// ignore parse errors
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Resolve an import from one file to another.
|
|
226
|
+
*
|
|
227
|
+
* @param fromAbsFile - Absolute path of the importing file
|
|
228
|
+
* @param modulePath - The import specifier (e.g., './auth', 'express', '@/lib/db')
|
|
229
|
+
* @param lang - Language key (ts, javascript, typescript, python, ruby, etc.)
|
|
230
|
+
* @param repoRoot - Absolute path to the repository root
|
|
231
|
+
* @param tsconfigAliases - Optional pre-loaded tsconfig aliases for TS/JS
|
|
232
|
+
* @returns Absolute path to the resolved file, or null if unresolvable
|
|
233
|
+
*/
|
|
234
|
+
export function resolveImport(fromAbsFile, modulePath, lang, repoRoot, tsconfigAliases) {
|
|
235
|
+
const resolver = RESOLVERS[lang];
|
|
236
|
+
if (!resolver) {
|
|
237
|
+
log.warn('No import resolver registered for language', { lang, module: modulePath, from: fromAbsFile });
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
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';
|
|
250
|
+
// Handle package.json #imports (TS/JS only)
|
|
251
|
+
if (isTsOrJs && modulePath.startsWith('#')) {
|
|
252
|
+
const result = resolveHashImport(modulePath, repoRoot);
|
|
253
|
+
if (result) {
|
|
254
|
+
try {
|
|
255
|
+
ensureWithinRoot(result, repoRoot);
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
log.warn('Import resolves outside repository root', {
|
|
260
|
+
from: fromAbsFile,
|
|
261
|
+
module: modulePath,
|
|
262
|
+
resolved: result,
|
|
263
|
+
});
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
let result = resolver(fromAbsFile, modulePath, repoRoot);
|
|
269
|
+
// Fallback: tsconfig aliases for TS/JS
|
|
270
|
+
if (!result && isTsOrJs && tsconfigAliases?.size) {
|
|
271
|
+
result = resolveWithAliases(modulePath, tsconfigAliases, repoRoot);
|
|
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
|
+
}
|
|
280
|
+
// Fallback: monorepo workspace exports for TS/JS bare specifiers
|
|
281
|
+
if (!result && isTsOrJs && !modulePath.startsWith('.')) {
|
|
282
|
+
result = resolveWorkspaceExport(modulePath, repoRoot);
|
|
283
|
+
}
|
|
284
|
+
// Validate resolved path is within repo root
|
|
285
|
+
if (result) {
|
|
286
|
+
try {
|
|
287
|
+
ensureWithinRoot(result, repoRoot);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
log.warn('Import resolves outside repository root', {
|
|
291
|
+
from: fromAbsFile,
|
|
292
|
+
module: modulePath,
|
|
293
|
+
resolved: result,
|
|
294
|
+
});
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
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
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
export { loadTsconfigAliases };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
2
|
+
import { dirname, join, resolve as resolvePath } from 'path';
|
|
3
|
+
import { cachedExists } from '../fs-cache';
|
|
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
|
+
}
|
|
50
|
+
export function resolve(_fromAbsFile, modulePath, repoRoot) {
|
|
51
|
+
if (STDLIB_PREFIXES.some((p) => modulePath.startsWith(p))) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
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);
|
|
58
|
+
// Try resolving as a .cs file first
|
|
59
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
60
|
+
const pathPart = segments.slice(i).join('/');
|
|
61
|
+
const candidate = `${pathPart}.cs`;
|
|
62
|
+
for (const base of standardBases) {
|
|
63
|
+
const full = join(repoRoot, base, candidate);
|
|
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)) {
|
|
72
|
+
return resolvePath(full);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Try resolving as a directory (namespace → folder mapping)
|
|
77
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
78
|
+
const pathPart = segments.slice(i).join('/');
|
|
79
|
+
for (const base of standardBases) {
|
|
80
|
+
const full = join(repoRoot, base, pathPart);
|
|
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()) {
|
|
89
|
+
return resolvePath(full);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|