-
Notifications
You must be signed in to change notification settings - Fork 4
Description
Found during dogfooding v3.3.2-dev.39
Severity: Critical
Command: All commands — import resolution is broken for TypeScript codebases
Reproduction
# From the codegraph repo root
node -e "
const { loadNative } = require('./dist/infrastructure/native.js');
const native = loadNative();
console.log(native.resolveImport('src/cli/commands/build.ts', '../../domain/graph/builder.js', '.'));
"
# Output: src/cli/commands/../../domain/graph/builder.js
# Expected: src/domain/graph/builder.tsAfter building a TypeScript codebase where imports use .js extensions (ESM convention: import { foo } from './bar.js' where bar.ts is the actual file), only 21 import edges are resolved out of hundreds of imports.
Expected behavior
resolveImport should return src/domain/graph/builder.ts — the .js → .ts remap logic (line 117-133 in import_resolution.rs) should trigger.
Actual behavior
Returns src/cli/commands/../../domain/graph/builder.js — the path is not normalized, causing strip_prefix(root_dir) to fail silently.
Root cause
In crates/codegraph-core/src/import_resolution.rs, line 114:
let resolved: PathBuf = dir.join(import_source).components().collect();PathBuf::components().collect() does NOT resolve .. components in Rust — it only normalizes . and separators. So when import_source is ../../domain/graph/builder.js, the resolved path retains the ../.. components.
The .js → .ts remap at lines 117-133 then fails because:
file_exists(&ts_candidate, None)may succeed (OS resolves..inexists()), BUTPath::new(&ts_candidate).strip_prefix(root)fails because the unnormalized pathsrc/cli/commands/../../domain/graph/builder.tscannot be prefix-stripped by.or an absolute root path.- The function falls through and returns the original
.jspath.
The same bug affects ALL extension probing logic that follows (lines 134+).
Suggested fix
Rust fix (primary)
Replace line 114 with proper path normalization:
fn clean_path(p: &Path) -> PathBuf {
let mut result = PathBuf::new();
for c in p.components() {
match c {
std::path::Component::ParentDir => { result.pop(); }
std::path::Component::CurDir => {}
_ => result.push(c),
}
}
result
}
let resolved = clean_path(&dir.join(import_source));JS workaround (immediate)
In resolveImportPath (resolve.ts), after getting the native result, check if it ends with .js and apply the JS .js → .ts fallback:
if (native) {
const result = native.resolveImport(fromFile, importSource, rootDir, ...);
const normalized = normalizePath(path.normalize(result));
if (normalized.endsWith('.js')) {
const tsPath = path.resolve(rootDir, normalized.replace(/\.js$/, '.ts'));
if (fs.existsSync(tsPath)) return normalizePath(path.relative(rootDir, tsPath));
const tsxPath = path.resolve(rootDir, normalized.replace(/\.js$/, '.tsx'));
if (fs.existsSync(tsxPath)) return normalizePath(path.relative(rootDir, tsxPath));
}
return normalized;
}This also affects resolve_imports_batch when called without known_files.