feat: resolve TypeScript tsconfig path aliases in import edges#155
feat: resolve TypeScript tsconfig path aliases in import edges#155nuthalapativarun wants to merge 4 commits intosafishamsi:v3from
Conversation
…hamsi#147) Add load_tsconfig_paths() to detect.py that walks up the directory tree to find tsconfig.json and extracts compilerOptions.paths. Add resolve_ts_alias() to map alias prefixes (e.g. @/*) to their resolved filesystem paths. Update extract_js() to load aliases for the file being processed and pass them through a closure to _import_js(), so aliased imports like @/components/Button resolve to real module names in the edge graph.
199c4a4 to
9505533
Compare
|
Ran graphify on a production React Native TypeScript codebase (~800 TS/TSX files, heavy use of Current behavior (main):
Root cause matches exactly what this PR fixes: in Would be very useful to have this merged. Happy to test the patch against the same corpus and report back the delta if that helps. |
|
Tested this PR end-to-end on a real TypeScript codebase (~1,400 TS/TSX files, 22 aliases in
Measured impact on a real corpus (after fixing locally): The naive regex strip ( raw = tsconfig_path.read_text(encoding="utf-8")
out: list[str] = []
i, n = 0, len(raw)
while i < n:
c = raw[i]
if c == '"':
j = i + 1
while j < n:
if raw[j] == "\\" and j + 1 < n:
j += 2
continue
if raw[j] == '"':
j += 1
break
j += 1
out.append(raw[i:j])
i = j
elif c == "/" and i + 1 < n and raw[i + 1] == "/":
nl = raw.find("\n", i)
i = n if nl == -1 else nl
elif c == "/" and i + 1 < n and raw[i + 1] == "*":
end = raw.find("*/", i + 2)
i = n if end == -1 else end + 2
else:
out.append(c)
i += 1
stripped = re.sub(r",\s*([}\]])", r"\1", "".join(out))
data = json.loads(stripped)Handles: Feel free to fold this into the PR — happy to open a follow-up PR against your branch if that's easier. |
Real-world tsconfig.json files use JSONC syntax (// and /* */ comments,
trailing commas) by default. The plain json.loads() call silently returned
{} on any such file, making alias resolution a no-op in practice.
Replace with a string-aware JSONC stripper that handles line comments,
block comments (including multi-line), trailing commas, and string
literals containing comment-like sequences.
|
Thanks @vhsantos26 — you're right on both counts. The I've pushed a fix in 3fbfa6f that replaces the bare The numbers you measured (imports_ratio 3% → 23.8%, isolated nodes -85%, communities -75%) are exactly the kind of signal I was hoping this would produce on a real corpus. Appreciate you testing it end-to-end. |
|
Hi, extract() returns cached JS/TS extraction results keyed only by the source file hash, but extract_js() now depends on tsconfig.json (paths/baseUrl). Changing tsconfig.json will not invalidate cached results, so import edges can remain wrong until the source files change or the cache is cleared. Severity: action required | Category: correctness How to fix: Include tsconfig in cache key Agent prompt to fix - you can give this to your LLM of choice:
We noticed a couple of other issues in this PR as well - happy to share if helpful. Qodo code review - free for open-source. |
extract_js() now resolves aliases from tsconfig.json, but the cache was keyed only on source file contents. A tsconfig change would leave stale import edges in the cache until the source files themselves changed. Add an extra_key parameter to file_hash/load_cached/save_cached. In the extraction loop, JS/TS files with a non-empty alias map hash the serialised alias map and mix it into the cache key, so any change to compilerOptions.paths or baseUrl triggers a cache miss.
|
Good catch @qodo-ai-reviewer — fixed in 586c1c9. The cache key now includes a SHA256 of the serialised alias map for JS/TS files. Happy to hear the other issues you found as well. |
Summary
Closes #147
Graphify previously treated aliased TypeScript imports (e.g.
@/components/Button) as opaque strings, producing broken or missing import edges for any project usingtsconfig.jsonpathsmappings (Next.js, Vite, Create React App, etc.).This PR adds tsconfig-aware alias resolution so those imports map to real module names in the graph.
Changes
graphify/detect.pyload_tsconfig_paths(root)— walks up the directory tree from the file being extracted to findtsconfig.json, parsescompilerOptions.baseUrlandcompilerOptions.paths, and returns analias_prefix → resolved_pathdictresolve_ts_alias(import_path, alias_map)— replaces a matching alias prefix with its resolved filesystem pathgraphify/extract.py_import_js()— accepts an optionalalias_mapkwarg; resolves any alias before deriving the module name for the edge targetextract_js()— callsload_tsconfig_paths()for the file's directory; if aliases are found, wraps_import_jsin a closure that captures the map and passes it asimport_handlerviadataclasses.replacetests/tests/fixtures/tsconfig_alias/— a minimal TypeScript project withtsconfig.jsonpaths and a source file using@/and@components/aliasestest_extract.py:load_tsconfig_pathsdiscovery, empty-map fallback,resolve_ts_aliasprefix matching, longer-prefix precedence, no-match pass-through, and an end-to-endextract_jstest (skipped if tree-sitter-typescript is not installed)Before / After
@(raw alias, broken)button,useauth(resolved module names)Notes
load_tsconfig_pathsis only called for.js/.ts/.tsxfiles and only when atsconfig.jsonexists in the ancestor tree. No performance impact on non-TypeScript projects../local,react, etc.) are completely unaffected.Test plan
test_load_tsconfig_paths_finds_config— finds tsconfig.json by walking up from subdirectorytest_load_tsconfig_paths_no_config— returns{}when no tsconfig existstest_resolve_ts_alias_replaces_prefix—@/hooks/useAuth→/project/src/hooks/useAuthtest_resolve_ts_alias_longer_prefix_wins—@components/Sidebaruses@componentsnot@test_resolve_ts_alias_no_match— relative and bare imports unchangedtest_extract_js_resolves_aliases— end-to-end: no raw@in edge targets