From ec04152a90a0d7ffd2f6cdfd6e0781eba28d2fb2 Mon Sep 17 00:00:00 2001 From: Jamie Evans Date: Thu, 11 Jun 2026 14:20:09 -0400 Subject: [PATCH] fix: resolve tsconfig path aliases relative to baseUrl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _read_tsconfig_aliases joined alias targets onto the tsconfig's own directory and ignored compilerOptions.baseUrl. In the common monorepo / NestJS layout (baseUrl "./src" with "@services/*": ["services/*"]), the alias resolved to /services instead of /src/services, so every aliased import failed to resolve and the import edge was silently dropped — leaving cross-file caller graphs nearly empty on alias-heavy TS repos. Resolve `paths` relative to `baseUrl` (TypeScript's actual semantics), defaulting to "." so configs without baseUrl keep their current behavior. Add a regression test covering a subdirectory baseUrl; the existing alias test only exercised baseUrl ".", which is why this slipped through. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 4 ++++ graphify/extract.py | 14 ++++++++++++-- tests/test_js_import_resolution.py | 21 +++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd125fab..5f0ccb5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases) +## Unreleased + +- Fix: tsconfig `paths` aliases are now resolved relative to `baseUrl`. `_read_tsconfig_aliases` previously joined alias targets onto the tsconfig's directory and ignored `compilerOptions.baseUrl`, so the common monorepo / NestJS layout (`baseUrl: "./src"` with `"@services/*": ["services/*"]`) resolved to `/services` instead of `/src/services` and every aliased import edge was silently dropped — leaving cross-file caller graphs nearly empty on alias-heavy TypeScript codebases. Resolution now joins `paths` onto `baseUrl` (defaulting to `.`, preserving prior behavior for configs without `baseUrl`). + ## 0.8.37 (2026-06-10) - Security: SSRF guard rewritten to eliminate thread-safety race. The global `socket.getaddrinfo` monkey-patch is replaced with per-connection `_SSRFGuardedHTTPConnection`/`_SSRFGuardedHTTPSConnection` subclasses that resolve DNS once, validate the IP, and connect to that exact address — closing both the concurrent-thread race window and the underlying TOCTOU gap. No global state is mutated, so sibling threads (MCP server, PR triage pool) are unaffected. diff --git a/graphify/extract.py b/graphify/extract.py index cf857bed..a8b217b0 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -255,13 +255,23 @@ def _read_tsconfig_aliases(tsconfig: Path, base_dir: Path, seen: set) -> dict[st if extended_path.exists(): aliases.update(_read_tsconfig_aliases(extended_path, extended_path.parent, seen)) - paths = data.get("compilerOptions", {}).get("paths", {}) + # tsconfig `paths` are resolved relative to `baseUrl` (itself relative to + # the tsconfig's directory), not the tsconfig directory directly. Honoring + # baseUrl is required for the common monorepo / NestJS layout where + # baseUrl points at a subdirectory, e.g. baseUrl "./src" with + # "@services/*": ["services/*"] must resolve to /src/services rather + # than /services. Defaults to "." so configs without baseUrl (paths + # relative to the tsconfig dir, the TS 4.1+ behavior) keep working. + compiler_options = data.get("compilerOptions", {}) + base_url = compiler_options.get("baseUrl") or "." + paths_base = base_dir / base_url + paths = compiler_options.get("paths", {}) for alias, targets in paths.items(): if not targets: continue alias_prefix = alias.rstrip("/*") target_base = targets[0].rstrip("/*") - aliases[alias_prefix] = str(base_dir / target_base) + aliases[alias_prefix] = str(os.path.normpath(paths_base / target_base)) return aliases diff --git a/tests/test_js_import_resolution.py b/tests/test_js_import_resolution.py index e2efbcee..6cf084c5 100644 --- a/tests/test_js_import_resolution.py +++ b/tests/test_js_import_resolution.py @@ -334,6 +334,27 @@ def test_tsconfig_alias_import_resolves_existing_ts_file(tmp_path: Path): assert _has_edge(result, "src/routes/page.ts", "src/lib/types/type-helpers.ts") +def test_tsconfig_alias_with_subdirectory_baseurl_resolves_existing_ts_file(tmp_path: Path): + # `paths` are resolved relative to `baseUrl`, which is commonly a + # subdirectory in monorepo / NestJS layouts (baseUrl "./src"). + # Regression: baseUrl was ignored, so "@services/*": ["services/*"] with + # baseUrl "./src" resolved to /services instead of /src/services, + # and every aliased import edge was silently dropped. + _write( + tmp_path / "tsconfig.json", + json.dumps({"compilerOptions": {"baseUrl": "./src", "paths": {"@services/*": ["services/*"]}}}), + ) + target = _write(tmp_path / "src/services/foo/index.ts", "export class Foo { id = '' }\n") + importer = _write( + tmp_path / "src/routes/page.ts", + "import { Foo } from '@services/foo'\nnew Foo()\n", + ) + + result = _extract_for([target, importer], tmp_path) + + assert _has_edge(result, "src/routes/page.ts", "src/services/foo/index.ts") + + def test_tsconfig_array_extends_alias_resolves_existing_ts_file(tmp_path: Path): # TypeScript 5.0 allows `extends` as an array; later entries override # earlier ones. The `paths` alias is inherited from the second parent.