@kodus/kodus-graph 0.2.7 → 0.2.9
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 +57 -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 +83 -0
- package/dist/analysis/diff.d.ts +35 -0
- package/dist/analysis/diff.js +140 -0
- package/dist/analysis/enrich.d.ts +5 -0
- package/dist/analysis/enrich.js +98 -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 +166 -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 +208 -0
- package/dist/commands/analyze.d.ts +9 -0
- package/dist/commands/analyze.js +114 -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 +10 -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 +2 -0
- package/dist/graph/builder.js +216 -0
- package/dist/graph/edges.d.ts +19 -0
- package/dist/graph/edges.js +105 -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 +249 -0
- package/dist/graph/types.js +1 -0
- package/dist/parser/batch.d.ts +4 -0
- package/dist/parser/batch.js +78 -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 +303 -0
- package/dist/resolver/call-resolver.d.ts +36 -0
- package/dist/resolver/call-resolver.js +178 -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 +212 -0
- package/dist/resolver/languages/csharp.d.ts +1 -0
- package/dist/resolver/languages/csharp.js +31 -0
- package/dist/resolver/languages/go.d.ts +3 -0
- package/dist/resolver/languages/go.js +196 -0
- package/dist/resolver/languages/java.d.ts +1 -0
- package/dist/resolver/languages/java.js +108 -0
- package/dist/resolver/languages/php.d.ts +3 -0
- package/dist/resolver/languages/php.js +54 -0
- package/dist/resolver/languages/python.d.ts +11 -0
- package/dist/resolver/languages/python.js +51 -0
- package/dist/resolver/languages/ruby.d.ts +9 -0
- package/dist/resolver/languages/ruby.js +59 -0
- package/dist/resolver/languages/rust.d.ts +1 -0
- package/dist/resolver/languages/rust.js +196 -0
- package/dist/resolver/languages/typescript.d.ts +27 -0
- package/dist/resolver/languages/typescript.js +240 -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 -131
- 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 -207
- package/src/commands/analyze.ts +0 -128
- package/src/commands/communities.ts +0 -19
- package/src/commands/context.ts +0 -139
- 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 -14
- 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,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruby import resolver.
|
|
3
|
+
*
|
|
4
|
+
* Handles require_relative paths and Gemfile path: gems.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a Ruby require/require_relative to a file path.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolve(fromAbsFile: string, modulePath: string, repoRoot: string): string | null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruby import resolver.
|
|
3
|
+
*
|
|
4
|
+
* Handles require_relative paths and Gemfile path: gems.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from 'fs';
|
|
7
|
+
import { dirname, join, resolve as resolvePath } from 'path';
|
|
8
|
+
/** Cache parsed Gemfile path gems per repo root. */
|
|
9
|
+
const gemfileCache = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Parse Gemfile for path: gems and return their lib directories (absolute).
|
|
12
|
+
*/
|
|
13
|
+
function getGemPathLibDirs(repoRoot) {
|
|
14
|
+
if (gemfileCache.has(repoRoot)) {
|
|
15
|
+
return gemfileCache.get(repoRoot);
|
|
16
|
+
}
|
|
17
|
+
const gemfilePath = join(repoRoot, 'Gemfile');
|
|
18
|
+
const libDirs = [];
|
|
19
|
+
if (existsSync(gemfilePath)) {
|
|
20
|
+
const content = readFileSync(gemfilePath, 'utf-8');
|
|
21
|
+
// Match lines like: gem 'mylib', path: './libs/mylib'
|
|
22
|
+
const regex = /^\s*gem\s+['"][^'"]+['"]\s*,\s*path:\s*['"]([^'"]+)['"]/gm;
|
|
23
|
+
let match = regex.exec(content);
|
|
24
|
+
while (match !== null) {
|
|
25
|
+
const gemPath = match[1];
|
|
26
|
+
libDirs.push(resolvePath(join(repoRoot, gemPath, 'lib')));
|
|
27
|
+
match = regex.exec(content);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
gemfileCache.set(repoRoot, libDirs);
|
|
31
|
+
return libDirs;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a Ruby require/require_relative to a file path.
|
|
35
|
+
*/
|
|
36
|
+
export function resolve(fromAbsFile, modulePath, repoRoot) {
|
|
37
|
+
if (!modulePath) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
// 1. Try relative resolution (require_relative style)
|
|
41
|
+
const base = join(dirname(fromAbsFile), modulePath);
|
|
42
|
+
if (existsSync(`${base}.rb`)) {
|
|
43
|
+
return resolvePath(`${base}.rb`);
|
|
44
|
+
}
|
|
45
|
+
if (existsSync(base)) {
|
|
46
|
+
return resolvePath(base);
|
|
47
|
+
}
|
|
48
|
+
// 2. Try Gemfile path: gems
|
|
49
|
+
for (const libDir of getGemPathLibDirs(repoRoot)) {
|
|
50
|
+
const candidate = join(libDir, modulePath);
|
|
51
|
+
if (existsSync(`${candidate}.rb`)) {
|
|
52
|
+
return resolvePath(`${candidate}.rb`);
|
|
53
|
+
}
|
|
54
|
+
if (existsSync(candidate)) {
|
|
55
|
+
return resolvePath(candidate);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolve(fromAbsFile: string, modulePath: string, repoRoot: string): string | null;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { basename, dirname, join, resolve as resolvePath } from 'path';
|
|
3
|
+
function probeRustPath(baseDir, relPath) {
|
|
4
|
+
const asFile = join(baseDir, `${relPath}.rs`);
|
|
5
|
+
if (existsSync(asFile)) {
|
|
6
|
+
return resolvePath(asFile);
|
|
7
|
+
}
|
|
8
|
+
const asMod = join(baseDir, relPath, 'mod.rs');
|
|
9
|
+
if (existsSync(asMod)) {
|
|
10
|
+
return resolvePath(asMod);
|
|
11
|
+
}
|
|
12
|
+
const asLib = join(baseDir, relPath, 'lib.rs');
|
|
13
|
+
if (existsSync(asLib)) {
|
|
14
|
+
return resolvePath(asLib);
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
export function resolve(fromAbsFile, modulePath, repoRoot) {
|
|
19
|
+
if (modulePath.startsWith('std::')) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (modulePath.startsWith('crate::')) {
|
|
23
|
+
const rest = modulePath.slice('crate::'.length).replace(/::/g, '/');
|
|
24
|
+
return probeRustPath(join(repoRoot, 'src'), rest);
|
|
25
|
+
}
|
|
26
|
+
if (modulePath.startsWith('super::')) {
|
|
27
|
+
const rest = modulePath.slice('super::'.length).replace(/::/g, '/');
|
|
28
|
+
const fileName = basename(fromAbsFile);
|
|
29
|
+
// mod.rs and lib.rs represent their parent directory, so super:: goes up two levels
|
|
30
|
+
const parentDir = fileName === 'mod.rs' || fileName === 'lib.rs' ? dirname(dirname(fromAbsFile)) : dirname(fromAbsFile);
|
|
31
|
+
return probeRustPath(parentDir, rest);
|
|
32
|
+
}
|
|
33
|
+
if (modulePath.startsWith('self::')) {
|
|
34
|
+
const rest = modulePath.slice('self::'.length).replace(/::/g, '/');
|
|
35
|
+
return probeRustPath(dirname(fromAbsFile), rest);
|
|
36
|
+
}
|
|
37
|
+
// Try workspace path dependency resolution
|
|
38
|
+
const firstSep = modulePath.indexOf('::');
|
|
39
|
+
if (firstSep !== -1) {
|
|
40
|
+
const crateName = modulePath.slice(0, firstSep);
|
|
41
|
+
const rest = modulePath.slice(firstSep + 2).replace(/::/g, '/');
|
|
42
|
+
const depPath = resolveWorkspacePathDep(fromAbsFile, crateName);
|
|
43
|
+
if (depPath) {
|
|
44
|
+
const srcDir = join(depPath, 'src');
|
|
45
|
+
// Try the full path first, then progressively strip trailing segments
|
|
46
|
+
// (they may be items like functions/structs inside a module file)
|
|
47
|
+
const segments = rest.split('/');
|
|
48
|
+
for (let i = segments.length; i >= 1; i--) {
|
|
49
|
+
const partial = segments.slice(0, i).join('/');
|
|
50
|
+
const result = probeRustPath(srcDir, partial);
|
|
51
|
+
if (result) {
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// bin+lib same crate: if the first segment matches the local [package] name,
|
|
57
|
+
// treat it like a crate:: import (resolve from src/)
|
|
58
|
+
const localPkgName = findLocalPackageName(fromAbsFile);
|
|
59
|
+
if (localPkgName && crateName === localPkgName) {
|
|
60
|
+
const srcDir = join(findCrateDir(fromAbsFile), 'src');
|
|
61
|
+
const segments = rest.split('/');
|
|
62
|
+
for (let i = segments.length; i >= 1; i--) {
|
|
63
|
+
const partial = segments.slice(0, i).join('/');
|
|
64
|
+
const result = probeRustPath(srcDir, partial);
|
|
65
|
+
if (result) {
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/** Cache: crate dir → parsed {depName → resolved absolute path} */
|
|
74
|
+
const pathDepCache = new Map();
|
|
75
|
+
/**
|
|
76
|
+
* Walk up from `fromAbsFile` to find the nearest Cargo.toml,
|
|
77
|
+
* parse its [dependencies] for `path = "..."` entries,
|
|
78
|
+
* and return the absolute path of the dependency crate if it matches `depName`.
|
|
79
|
+
*/
|
|
80
|
+
function resolveWorkspacePathDep(fromAbsFile, depName) {
|
|
81
|
+
const crateDir = findCrateDir(fromAbsFile);
|
|
82
|
+
if (!crateDir) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
let deps = pathDepCache.get(crateDir);
|
|
86
|
+
if (!deps) {
|
|
87
|
+
deps = parsePathDeps(crateDir);
|
|
88
|
+
pathDepCache.set(crateDir, deps);
|
|
89
|
+
}
|
|
90
|
+
return deps.get(depName) ?? null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Walk up from a file to find the nearest directory containing Cargo.toml.
|
|
94
|
+
*/
|
|
95
|
+
function findCrateDir(fromAbsFile) {
|
|
96
|
+
let dir = dirname(fromAbsFile);
|
|
97
|
+
const root = resolvePath('/');
|
|
98
|
+
while (dir !== root) {
|
|
99
|
+
if (existsSync(join(dir, 'Cargo.toml'))) {
|
|
100
|
+
return dir;
|
|
101
|
+
}
|
|
102
|
+
const parent = dirname(dir);
|
|
103
|
+
if (parent === dir) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
dir = parent;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/** Cache: crate dir → parsed package name */
|
|
111
|
+
const pkgNameCache = new Map();
|
|
112
|
+
/**
|
|
113
|
+
* Find the [package] name from the nearest Cargo.toml for the given file.
|
|
114
|
+
* Used to detect bin+lib same-crate imports (e.g. `use myapp::foo` from main.rs).
|
|
115
|
+
*/
|
|
116
|
+
function findLocalPackageName(fromAbsFile) {
|
|
117
|
+
const crateDir = findCrateDir(fromAbsFile);
|
|
118
|
+
if (!crateDir) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const cached = pkgNameCache.get(crateDir);
|
|
122
|
+
if (cached !== undefined) {
|
|
123
|
+
return cached;
|
|
124
|
+
}
|
|
125
|
+
const cargoPath = join(crateDir, 'Cargo.toml');
|
|
126
|
+
if (!existsSync(cargoPath)) {
|
|
127
|
+
pkgNameCache.set(crateDir, null);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const content = readFileSync(cargoPath, 'utf-8');
|
|
131
|
+
const match = content.match(/^\s*name\s*=\s*"([^"]+)"/m);
|
|
132
|
+
// Cargo crate names use hyphens but Rust imports use underscores
|
|
133
|
+
const name = match ? match[1].replace(/-/g, '_') : null;
|
|
134
|
+
pkgNameCache.set(crateDir, name);
|
|
135
|
+
return name;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Parse Cargo.toml in `crateDir` for path dependencies.
|
|
139
|
+
* Returns a map of dependency name → resolved absolute directory.
|
|
140
|
+
*
|
|
141
|
+
* Handles both inline table and multi-line table forms:
|
|
142
|
+
* shared = { path = "../shared" }
|
|
143
|
+
* [dependencies.shared]
|
|
144
|
+
* path = "../shared"
|
|
145
|
+
*/
|
|
146
|
+
function parsePathDeps(crateDir) {
|
|
147
|
+
const result = new Map();
|
|
148
|
+
const cargoPath = join(crateDir, 'Cargo.toml');
|
|
149
|
+
if (!existsSync(cargoPath)) {
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
const content = readFileSync(cargoPath, 'utf-8');
|
|
153
|
+
const lines = content.split('\n');
|
|
154
|
+
let inDepsSection = false;
|
|
155
|
+
let depsTableDep = null; // for [dependencies.foo] style
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
const trimmed = line.trim();
|
|
158
|
+
// Detect section headers
|
|
159
|
+
if (trimmed.startsWith('[')) {
|
|
160
|
+
// Check for [dependencies.foo] form
|
|
161
|
+
const subMatch = trimmed.match(/^\[dependencies\.(\S+)\]$/);
|
|
162
|
+
if (subMatch) {
|
|
163
|
+
depsTableDep = subMatch[1];
|
|
164
|
+
inDepsSection = false;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
depsTableDep = null;
|
|
168
|
+
if (trimmed === '[dependencies]') {
|
|
169
|
+
inDepsSection = true;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
// Any other section header ends [dependencies]
|
|
173
|
+
inDepsSection = false;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// Inside [dependencies.foo], look for path = "..."
|
|
177
|
+
if (depsTableDep) {
|
|
178
|
+
const pathMatch = trimmed.match(/^path\s*=\s*"([^"]+)"/);
|
|
179
|
+
if (pathMatch) {
|
|
180
|
+
const resolved = resolvePath(crateDir, pathMatch[1]);
|
|
181
|
+
result.set(depsTableDep, resolved);
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// Inside [dependencies], look for inline table with path
|
|
186
|
+
if (inDepsSection && trimmed.length > 0 && !trimmed.startsWith('#')) {
|
|
187
|
+
// name = { path = "..." ... }
|
|
188
|
+
const inlineMatch = trimmed.match(/^(\S+)\s*=\s*\{[^}]*path\s*=\s*"([^"]+)"[^}]*\}/);
|
|
189
|
+
if (inlineMatch) {
|
|
190
|
+
const resolved = resolvePath(crateDir, inlineMatch[2]);
|
|
191
|
+
result.set(inlineMatch[1], resolved);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript/JavaScript import resolver.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Relative imports with extension probing (.ts, .tsx, .js, .jsx)
|
|
6
|
+
* - ESM .js → .ts remapping
|
|
7
|
+
* - Directory index files
|
|
8
|
+
* - tsconfig path aliases
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a TypeScript/JavaScript relative import to an absolute file path.
|
|
12
|
+
* Returns null for non-relative (external package) imports.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolve(fromAbsFile: string, modulePath: string, repoRoot: string): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Load and parse tsconfig.json path aliases.
|
|
17
|
+
*
|
|
18
|
+
* Tries tsconfig.json first, then tsconfig.base.json.
|
|
19
|
+
* Converts alias patterns like "@libs/*" into prefix → resolved dirs.
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadTsconfigAliases(repoRoot: string): Map<string, string[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve an import path using tsconfig aliases.
|
|
24
|
+
*
|
|
25
|
+
* Tries each alias prefix, and for matches, probes extensions and index files.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveWithAliases(modulePath: string, aliases: Map<string, string[]>, _repoRoot: string): string | null;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript/JavaScript import resolver.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Relative imports with extension probing (.ts, .tsx, .js, .jsx)
|
|
6
|
+
* - ESM .js → .ts remapping
|
|
7
|
+
* - Directory index files
|
|
8
|
+
* - tsconfig path aliases
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync } from 'fs';
|
|
11
|
+
import { dirname, join, resolve as resolvePath } from 'path';
|
|
12
|
+
import { log } from '../../shared/logger';
|
|
13
|
+
const TS_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
|
|
14
|
+
/**
|
|
15
|
+
* Probe a base path for TS/JS files: try extensions, then index files.
|
|
16
|
+
* Returns the resolved absolute path or null.
|
|
17
|
+
*/
|
|
18
|
+
function probeExtensions(base) {
|
|
19
|
+
for (const ext of TS_EXTENSIONS) {
|
|
20
|
+
const candidate = base + ext;
|
|
21
|
+
if (existsSync(candidate)) {
|
|
22
|
+
return resolvePath(candidate);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const ext of TS_EXTENSIONS) {
|
|
26
|
+
const candidate = join(base, `index${ext}`);
|
|
27
|
+
if (existsSync(candidate)) {
|
|
28
|
+
return resolvePath(candidate);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/** Cache for parsed tsconfig.json (keyed by repoRoot). */
|
|
34
|
+
const tsconfigCache = new Map();
|
|
35
|
+
function loadTsconfigCompilerOptions(repoRoot) {
|
|
36
|
+
const cached = tsconfigCache.get(repoRoot);
|
|
37
|
+
if (cached !== undefined) {
|
|
38
|
+
return cached;
|
|
39
|
+
}
|
|
40
|
+
const tsconfigPath = join(repoRoot, 'tsconfig.json');
|
|
41
|
+
let result = {};
|
|
42
|
+
if (existsSync(tsconfigPath)) {
|
|
43
|
+
try {
|
|
44
|
+
const content = readFileSync(tsconfigPath, 'utf-8');
|
|
45
|
+
const cleaned = stripJsonComments(content);
|
|
46
|
+
const config = JSON.parse(cleaned);
|
|
47
|
+
const rootDirs = config?.compilerOptions?.rootDirs;
|
|
48
|
+
if (Array.isArray(rootDirs)) {
|
|
49
|
+
result = { rootDirs };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// ignore parse errors
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
tsconfigCache.set(repoRoot, result);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve a TypeScript/JavaScript relative import to an absolute file path.
|
|
61
|
+
* Returns null for non-relative (external package) imports.
|
|
62
|
+
*/
|
|
63
|
+
export function resolve(fromAbsFile, modulePath, repoRoot) {
|
|
64
|
+
// Strip Vite-style query suffixes (?raw, ?url, ?worker, etc.)
|
|
65
|
+
const queryIdx = modulePath.indexOf('?');
|
|
66
|
+
if (queryIdx !== -1) {
|
|
67
|
+
modulePath = modulePath.slice(0, queryIdx);
|
|
68
|
+
}
|
|
69
|
+
if (!modulePath.startsWith('.')) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
let base = join(dirname(fromAbsFile), modulePath);
|
|
73
|
+
// If the path has a non-TS/JS extension (e.g. .txt, .svg), try exact match
|
|
74
|
+
if (/\.\w+$/.test(modulePath) && !TS_EXTENSIONS.some((ext) => modulePath.endsWith(ext))) {
|
|
75
|
+
if (existsSync(base)) {
|
|
76
|
+
return resolvePath(base);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ESM convention: .js in import -> .ts on disk
|
|
80
|
+
if (modulePath.endsWith('.js')) {
|
|
81
|
+
base = base.slice(0, -3);
|
|
82
|
+
}
|
|
83
|
+
const direct = probeExtensions(base);
|
|
84
|
+
if (direct) {
|
|
85
|
+
return direct;
|
|
86
|
+
}
|
|
87
|
+
// rootDirs fallback: try the same relative import from other root directories
|
|
88
|
+
const { rootDirs } = loadTsconfigCompilerOptions(repoRoot);
|
|
89
|
+
if (rootDirs && rootDirs.length > 0) {
|
|
90
|
+
const fromDir = dirname(fromAbsFile);
|
|
91
|
+
for (const rd of rootDirs) {
|
|
92
|
+
const absRd = resolvePath(repoRoot, rd);
|
|
93
|
+
// Check if fromDir is inside this rootDir
|
|
94
|
+
if (fromDir.startsWith(`${absRd}/`) || fromDir === absRd) {
|
|
95
|
+
const relFromRoot = fromDir.slice(absRd.length); // e.g. "" or "/sub"
|
|
96
|
+
// Try the same relative path under each other rootDir
|
|
97
|
+
for (const otherRd of rootDirs) {
|
|
98
|
+
if (otherRd === rd) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const absOtherRd = resolvePath(repoRoot, otherRd);
|
|
102
|
+
const relModule = modulePath.startsWith('./') ? modulePath.slice(2) : modulePath;
|
|
103
|
+
let otherBase = join(absOtherRd, relFromRoot, relModule);
|
|
104
|
+
if (modulePath.endsWith('.js')) {
|
|
105
|
+
otherBase = otherBase.slice(0, -3);
|
|
106
|
+
}
|
|
107
|
+
const result = probeExtensions(otherBase);
|
|
108
|
+
if (result) {
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Strip comments and trailing commas from JSON (tsconfig-compatible).
|
|
120
|
+
* Handles strings correctly -- won't strip // inside "url://..." etc.
|
|
121
|
+
*/
|
|
122
|
+
function stripJsonComments(str) {
|
|
123
|
+
let result = '';
|
|
124
|
+
let i = 0;
|
|
125
|
+
const len = str.length;
|
|
126
|
+
while (i < len) {
|
|
127
|
+
// String literal -- copy as-is
|
|
128
|
+
if (str[i] === '"') {
|
|
129
|
+
let j = i + 1;
|
|
130
|
+
while (j < len && str[j] !== '"') {
|
|
131
|
+
if (str[j] === '\\') {
|
|
132
|
+
j++; // skip escaped char
|
|
133
|
+
}
|
|
134
|
+
j++;
|
|
135
|
+
}
|
|
136
|
+
result += str.substring(i, j + 1);
|
|
137
|
+
i = j + 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// Single-line comment
|
|
141
|
+
if (str[i] === '/' && str[i + 1] === '/') {
|
|
142
|
+
while (i < len && str[i] !== '\n') {
|
|
143
|
+
i++;
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// Block comment
|
|
148
|
+
if (str[i] === '/' && str[i + 1] === '*') {
|
|
149
|
+
i += 2;
|
|
150
|
+
while (i < len && !(str[i] === '*' && str[i + 1] === '/')) {
|
|
151
|
+
i++;
|
|
152
|
+
}
|
|
153
|
+
i += 2;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
// Trailing comma: comma followed by optional whitespace + closing bracket
|
|
157
|
+
if (str[i] === ',') {
|
|
158
|
+
let j = i + 1;
|
|
159
|
+
while (j < len && (str[j] === ' ' || str[j] === '\t' || str[j] === '\n' || str[j] === '\r')) {
|
|
160
|
+
j++;
|
|
161
|
+
}
|
|
162
|
+
if (str[j] === '}' || str[j] === ']') {
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
result += str[i];
|
|
168
|
+
i++;
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Load and parse tsconfig.json path aliases.
|
|
174
|
+
*
|
|
175
|
+
* Tries tsconfig.json first, then tsconfig.base.json.
|
|
176
|
+
* Converts alias patterns like "@libs/*" into prefix → resolved dirs.
|
|
177
|
+
*/
|
|
178
|
+
export function loadTsconfigAliases(repoRoot) {
|
|
179
|
+
const aliases = new Map();
|
|
180
|
+
for (const filename of ['tsconfig.json', 'tsconfig.base.json']) {
|
|
181
|
+
const tsconfigPath = join(repoRoot, filename);
|
|
182
|
+
if (!existsSync(tsconfigPath)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const content = readFileSync(tsconfigPath, 'utf-8');
|
|
187
|
+
const cleaned = stripJsonComments(content);
|
|
188
|
+
const config = JSON.parse(cleaned);
|
|
189
|
+
const paths = config?.compilerOptions?.paths;
|
|
190
|
+
const baseUrl = config?.compilerOptions?.baseUrl || '.';
|
|
191
|
+
const baseDir = join(repoRoot, baseUrl);
|
|
192
|
+
if (paths) {
|
|
193
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
194
|
+
// Convert alias pattern: "@libs/*" -> prefix "@libs/"
|
|
195
|
+
const prefix = alias.replace('/*', '/').replace('*', '');
|
|
196
|
+
const resolvedTargets = targets.map((t) => {
|
|
197
|
+
const targetPath = t.replace('/*', '').replace('*', '');
|
|
198
|
+
return join(baseDir, targetPath);
|
|
199
|
+
});
|
|
200
|
+
aliases.set(prefix, resolvedTargets);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
log.warn('Failed to parse tsconfig', { file: tsconfigPath, error: String(err) });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return aliases;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Resolve an import path using tsconfig aliases.
|
|
212
|
+
*
|
|
213
|
+
* Tries each alias prefix, and for matches, probes extensions and index files.
|
|
214
|
+
*/
|
|
215
|
+
export function resolveWithAliases(modulePath, aliases, _repoRoot) {
|
|
216
|
+
for (const [prefix, targets] of aliases) {
|
|
217
|
+
if (modulePath.startsWith(prefix)) {
|
|
218
|
+
const rest = modulePath.slice(prefix.length);
|
|
219
|
+
for (const targetBase of targets) {
|
|
220
|
+
const base = join(targetBase, rest);
|
|
221
|
+
for (const ext of TS_EXTENSIONS) {
|
|
222
|
+
if (existsSync(base + ext)) {
|
|
223
|
+
return resolvePath(base + ext);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
for (const ext of TS_EXTENSIONS) {
|
|
227
|
+
const idx = join(base, `index${ext}`);
|
|
228
|
+
if (existsSync(idx)) {
|
|
229
|
+
return resolvePath(idx);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Try exact match (for directories with index)
|
|
233
|
+
if (existsSync(base)) {
|
|
234
|
+
return resolvePath(base);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export (barrel) resolver.
|
|
3
|
+
*
|
|
4
|
+
* Follows `export { X } from './module'` chains so that the import map
|
|
5
|
+
* points to the file where the symbol is actually defined, not the
|
|
6
|
+
* barrel index file.
|
|
7
|
+
*
|
|
8
|
+
* Without this, an import like `import { Foo } from '@lib'` resolves
|
|
9
|
+
* to `@lib/index.ts`, but `Foo` is defined in `@lib/foo.ts`.
|
|
10
|
+
* The call resolver can't find `Foo` in the barrel's symbol table and
|
|
11
|
+
* falls to lower-confidence tiers.
|
|
12
|
+
*/
|
|
13
|
+
interface RawReExport {
|
|
14
|
+
module: string;
|
|
15
|
+
file: string;
|
|
16
|
+
line: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build a map: barrel file (relative) → list of resolved re-export target files (relative).
|
|
20
|
+
*
|
|
21
|
+
* Follows one level of re-exports (covers >95% of real-world barrel patterns).
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildReExportMap(reExports: RawReExport[], repoDir: string, tsconfigAliases?: Map<string, string[]>): Map<string, string[]>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export (barrel) resolver.
|
|
3
|
+
*
|
|
4
|
+
* Follows `export { X } from './module'` chains so that the import map
|
|
5
|
+
* points to the file where the symbol is actually defined, not the
|
|
6
|
+
* barrel index file.
|
|
7
|
+
*
|
|
8
|
+
* Without this, an import like `import { Foo } from '@lib'` resolves
|
|
9
|
+
* to `@lib/index.ts`, but `Foo` is defined in `@lib/foo.ts`.
|
|
10
|
+
* The call resolver can't find `Foo` in the barrel's symbol table and
|
|
11
|
+
* falls to lower-confidence tiers.
|
|
12
|
+
*/
|
|
13
|
+
import { relative, resolve } from 'path';
|
|
14
|
+
import { resolveImport } from './import-resolver';
|
|
15
|
+
/**
|
|
16
|
+
* Build a map: barrel file (relative) → list of resolved re-export target files (relative).
|
|
17
|
+
*
|
|
18
|
+
* Follows one level of re-exports (covers >95% of real-world barrel patterns).
|
|
19
|
+
*/
|
|
20
|
+
export function buildReExportMap(reExports, repoDir, tsconfigAliases) {
|
|
21
|
+
const barrelMap = new Map();
|
|
22
|
+
for (const re of reExports) {
|
|
23
|
+
const absFrom = resolve(repoDir, re.file);
|
|
24
|
+
const resolved = resolveImport(absFrom, re.module, 'typescript', repoDir, tsconfigAliases);
|
|
25
|
+
if (!resolved) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const resolvedRel = relative(repoDir, resolved);
|
|
29
|
+
const list = barrelMap.get(re.file);
|
|
30
|
+
if (list) {
|
|
31
|
+
if (!list.includes(resolvedRel)) {
|
|
32
|
+
list.push(resolvedRel);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
barrelMap.set(re.file, [resolvedRel]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Follow one extra level: if a re-export target is itself a barrel, flatten
|
|
40
|
+
for (const [barrel, targets] of barrelMap) {
|
|
41
|
+
const extra = [];
|
|
42
|
+
for (const target of targets) {
|
|
43
|
+
const nested = barrelMap.get(target);
|
|
44
|
+
if (nested) {
|
|
45
|
+
for (const n of nested) {
|
|
46
|
+
if (n !== barrel && !targets.includes(n) && !extra.includes(n)) {
|
|
47
|
+
extra.push(n);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (extra.length > 0) {
|
|
53
|
+
targets.push(...extra);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return barrelMap;
|
|
57
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol table with dual-index lookup (GitNexus pattern).
|
|
3
|
+
*
|
|
4
|
+
* Provides both exact (file + name) and global (name-only) lookups.
|
|
5
|
+
* The exact lookup is high-confidence; global lookup is lower-confidence
|
|
6
|
+
* but useful when import resolution fails.
|
|
7
|
+
*/
|
|
8
|
+
export interface SymbolTable {
|
|
9
|
+
add(file: string, name: string, qualified: string): void;
|
|
10
|
+
lookupExact(file: string, name: string): string | null;
|
|
11
|
+
lookupInFile(file: string, name: string, className: string): string | null;
|
|
12
|
+
isUnique(name: string): boolean;
|
|
13
|
+
lookupGlobal(name: string): string[];
|
|
14
|
+
readonly size: number;
|
|
15
|
+
readonly fileCount: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function createSymbolTable(): SymbolTable;
|