Skip to content

fix(resolution): expand tsconfig ${configDir} in baseUrl and paths (#2340) - #2355

Open
Rishet11 wants to merge 1 commit into
Graphify-Labs:v8from
Rishet11:fix/2340-tsconfig-configdir
Open

fix(resolution): expand tsconfig ${configDir} in baseUrl and paths (#2340)#2355
Rishet11 wants to merge 1 commit into
Graphify-Labs:v8from
Rishet11:fix/2340-tsconfig-configdir

Conversation

@Rishet11

Copy link
Copy Markdown
Contributor

Closes #2340

The bug

TypeScript 5.5 allows the literal ${configDir} template in compilerOptions.baseUrl and in paths values. _read_tsconfig_aliases (graphify/extractors/resolution.py:150-161) and _load_tsconfig_base_url (:241-243) joined it as a plain path segment. With {"baseUrl": "${configDir}", "paths": {"@/*": ["${configDir}/src/*"]}} the token landed in the result twice — once from baseUrl, once from the paths value:

before: {"@/*": ["/private/tmp/cfgdir-repro/${configDir}/${configDir}/src/*"]}
        baseUrl: /private/tmp/cfgdir-repro/${configDir}
after:  {"@/*": ["/private/tmp/cfgdir-repro/src/*"]}
        baseUrl: /private/tmp/cfgdir-repro

Every aliased import resolved to a directory that cannot exist, so the edge was dropped. End to end through the real CLI on a two-file fixture:

base: 0 non-contains edges
fix:  imports_from src_app -> src_utils_helper
      imports      src_app -> src_utils_helper_helper

The fix

  • config_dir is threaded through _read_tsconfig_aliases, set once at the top-level entry to the directory of the originating config and passed unchanged through the extends recursion. Per spec the token expands relative to the originating config, not to whichever file in the chain declares the option — a test asserts exactly that.
  • _expand_config_dir_token substitutes only that exact token (no general ${...} or env-var expansion), only at the start of a value, and strips any leading ./ first since the substituted result is absolute.
  • A token anywhere else in a value is left literal rather than spliced mid-path. tsc does not accept it there, and substituting would fabricate a nonsense path; leaving it alone keeps the pre-fix outcome for such a config (the alias simply does not resolve) instead of inventing a wrong edge.
  • Same substitution in _load_tsconfig_base_url, using the directory of the config it actually found.

Scope, stated rather than hidden

_load_tsconfig_base_url reads only a single config file and does not follow extends, so a baseUrl inherited from a base config is invisible to it with or without this token. That is #2200 and I deliberately left it there rather than widening this PR.

I also checked the other places the TS spec permits the template: baseUrl and paths values are the two this code reads and both are fixed; the extends specifier itself does not support the token per spec, and include/outDir are never read here. So there is no third site.

Verification

  • 8 tests in tests/test_jsconfig_baseurl.py: token in baseUrl + paths together; token in a paths value with no baseUrl; token in baseUrl only; the spec-critical extends case asserting expansion to the originating dir and not the base config dir; a jsconfig.json variant; a no-token regression guard; plus ./ and .// prefix variants and the mid-value-left-literal case.
  • Red before the fix: stashing only resolution.py, 5 of the first 6 fail (the no-token guard passes either way by design).
  • After: 19 passed in that file.
  • Full suite: 3903 passed / 3 skipped / 4 failed, and the same 4 fail on a clean v8 tree here (1 in test_llm_backends.py from a missing openai module, 3 env-dependent ollama backend-detection tests). Failure set unchanged, verified by stashing and re-running.
  • ruff clean. python -m tools.skillgen --checkcheck OK: 134 artifact(s) match committed output and expected/.

A fresh-eyes review pass confirmed the extends threading passes the original config_dir (not extended_path.parent) at every level, that _load_tsconfig_base_url uses the found config's directory rather than the walk start dir, that the alias/baseUrl caches cannot return a value expanded against a different directory, and that the target-patterns comprehension drops no target. It flagged two edge cases in the original ./-stripping — .// prefixes and a mid-value token both produced wrong paths — which is why the substitution is now start-anchored with full ./ stripping, with tests for both.

…raphify-Labs#2340)

TypeScript 5.5 allows the literal ${configDir} template in
compilerOptions.baseUrl and in paths values, expanding to the directory of
the originating config (the root of the extends chain, not the file that
declares the option). _read_tsconfig_aliases and _load_tsconfig_base_url
joined it as a plain path segment, so with baseUrl and a paths value both
using the token it appeared twice in the result and every aliased import
resolved to a directory that cannot exist.

Thread the originating config's directory through _read_tsconfig_aliases,
unchanged across the extends recursion as the spec requires, and substitute
the token before the join in both functions. Only that exact token is
expanded, only at the start of a value, and any leading ./ is dropped first
since the substitution yields an absolute path. A token elsewhere in a value
is left literal: tsc does not accept it there, so failing to resolve is
better than fabricating a path.

baseUrl inherited through extends is still not followed by
_load_tsconfig_base_url; that is Graphify-Labs#2200 and is left alone.
Copilot AI review requested due to automatic review settings July 31, 2026 21:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

This PR adds support for TypeScript 5.5's ${configDir} template token in baseUrl and paths values within tsconfig/jsconfig alias resolution. A new _expand_config_dir_token helper substitutes the literal token with the originating config's directory, and _read_tsconfig_aliases now threads a config_dir parameter through the extends chain so the token always expands to the top-level config's location rather than the file that declares the option. The _load_tsconfig_base_url path and paths-joining logic were also updated to handle the now-possibly-absolute expanded values. The test file adds a suite of cases covering the token in baseUrl, paths, jsconfig variants, extends chains, ./-prefix handling, mid-value literals, and a no-token regression guard.

No blocking issues surfaced. 2 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 879 functions depend on the 143 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: extract() — 359 callers, 28 callees
  • worse: _load_tsconfig_base_url() — 5 callers, 3 callees
  • worse: _load_tsconfig_aliases() — 6 callers, 2 callees

Verification — 879 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 556 function(s) in the blast radius were not formally verified this run

· 3 more finding(s) on lines outside this diff (see the check run).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JS/TS resolution: ${configDir} in tsconfig paths is joined literally — every aliased import in a TS 5.5 monorepo is dropped

2 participants