fix(resolution): load path aliases through tsconfig extends and base configs (#1534) - #1548
fix(resolution): load path aliases through tsconfig extends and base configs (#1534)#1548maxmilian wants to merge 1 commit into
extends and base configs (#1534)#1548Conversation
…e configs (colbymchenry#1534) `loadProjectAliases()` read only the root `tsconfig.json` / `jsconfig.json` own `compilerOptions`, so an Nx-style monorepo — every alias declared in a `tsconfig.base.json` — got `null` back and every cross-package import fell through to name-based matching. Silently: no unresolved-import warning, and the results still look precise. Two things were missing, and either one alone leaves a common Nx layout broken: Fold the `extends` chain into the effective options before building the alias map. Relative and `node_modules` package specifiers both resolve, the nearest config wins (tsc replaces `paths` rather than merging), and a config already on the current chain is not re-entered, so `a extends b extends a` terminates instead of recursing forever. `paths` are anchored at `baseUrl` when one is declared — itself relative to the config that declared it — and otherwise at the directory of the config that declared the `paths`, which is what tsc does and what keeps an inherited `src/*` from being read as root-relative. Read `tsconfig.base.json` as a last candidate. A root `tsconfig.json` is still authoritative when it exists and reaches the base through `extends`; the fallback covers the layouts where that never happens — a solution-style root config (`references`, no `extends`, no `paths`, which is what nx's own repository ships) or no root `tsconfig.json` at all. A candidate that contributes no aliases no longer shadows a later one that does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
CodeGraph review
Overall risk: 🟠 Medium — Alias loading now walks inherited tsconfig chains for every import-resolution surface; the behavior change is broad, but the new test file exercises the added paths and fallbacks.
Worth double-checking
- Config precedence and fallback ordering
- Inherited path anchoring
- Indexing surfaces that consume alias resolution
What to look for in each
- Config precedence and fallback ordering — Scrutinize src/resolution/path-aliases.ts through line 278, where
loadProjectAliaseskeeps scanningtsconfig.json,jsconfig.json, thentsconfig.base.jsonuntil it findspaths; confirm mixed-config projects still prefer the intended root config and only use the base-file fallback when earlier configs truly contribute no aliases. - Inherited path anchoring — Check src/resolution/path-aliases.ts through line 217 together with
applyAliases: inheritedbaseUrlis resolved from the declaring config and, when absent, alias targets fall back topathsDir. That is the load-bearing behavior for nested base configs and package-provided tsconfigs. - Indexing surfaces that consume alias resolution — Because
src/resolution/index.tsis the only affected downstream file in the graph, do a final pass on CLI/MCP/library flows that rely on import resolution rather than symbol-name fallback, especially cross-package references in TypeScript workspaces.
Business rules — 1 honored · 2 not applicable
| Status | Rule | Note |
|---|---|---|
| ✔ Honored | Changelog and release contract | The changelog change adds a user-facing fix entry under ## [Unreleased] and ### Fixes without staging a version bump. |
| — Not applicable | Surfaces | The change affects internal alias resolution used by indexing, but it does not modify the CLI, MCP, or library surface definitions in src/resolution/index.ts. |
| — Not applicable | Source strings must exclude interpolated template literals | This patch only changes tsconfig alias loading and tests; it does not modify source-string extraction or template-literal indexing behavior. |
Full assessment
This change teaches project alias loading to follow TypeScript extends chains, resolve package-based inherited configs, anchor inherited paths to the config that declared them, and fall back to tsconfig.base.json for Nx-style roots. That should correct cross-package alias resolution in monorepos instead of silently falling back to same-name symbol matching. No concrete defect is visible in the patched source, but the merge should scrutinize precedence when tsconfig.json, jsconfig.json, and tsconfig.base.json coexist because that branch now decides which alias map every indexing surface uses.
QA checklist — 3 things to verify in the running product
- cli — Create or use a workspace whose root
tsconfig.jsononly contains project references and whose aliases live intsconfig.base.json, then run a fresh index and inspect impact/callers for a symbol imported from another package via an@scope/...path. (Before this PR that root layout produced no alias map, so cross-package imports fell back to name matching; after this PR the tool should follow the base config and connect the actual package file.) - cli — In a workspace where the root config extends another config that itself extends a third file, index the repo and query a cross-package import that depends on a wildcard alias from the deepest config. (This specifically verifies the new multi-hop
extendstraversal; before the PR only the root file's owncompilerOptionswere read, so the aliased import would not resolve through the inherited chain.) - cli — Use a repo where aliases come from a config package under
node_modules, then index and inspect a symbol reached through that alias path. (The new resolver now treats bareextendsvalues as package specifiers and searches upward throughnode_modules; reverting this PR would lose those aliases and change the indexed dependency edges.)
Blast radius: 1 file affected beyond the diff · 12 symbols · 1 test file selected
Tests to run:
__tests__/tsconfig-extends-aliases.test.ts
Fixes #1534. Covers both of the suggestions in the issue — they turn out to be
two halves of the same bug, and either one alone leaves a common Nx layout broken.
What was wrong
loadProjectAliases()read only the roottsconfig.json/jsconfig.json's owncompilerOptions. The header comment said as much:Nx-style monorepos declare every alias in a
tsconfig.base.json. Depending on thegeneration of the workspace, the root either inherits it through
extendsor doesnot reference it at all — so those projects got
nullback, no aliases whatsoever,and every cross-package import fell through to name-based matching. Silently: no
unresolved-import warning, and results still look precise, which is what makes it
worth fixing rather than documenting.
What this does
1. Folds the
extendschain into a single set of effective options beforebuilding the alias map:
./tsconfig.base,./tsconfig.base.json,@tsconfig/node18,@acme/configs/tsconfig.json—a missing
.jsonis implied and a bare package name means itstsconfig.json,resolved by walking up
node_modulesfrom the referencing config.pathsreplaces the parent's rather thanmerging, matching
tsc.so
a extends b extends astops instead of recursing forever (plus a depth cap).pathsare anchored correctly. AtbaseUrlwhen one is declared — itselfrelative to the config that declared it, not to the chain's entry point — and
otherwise at the directory of the config that declared the
paths. This is theeasy thing to get wrong: an inherited
"~/*": ["src/*"]living one directorydown must not be read as root-relative.
2. Adds
tsconfig.base.jsonas a candidate — last in the list, aftertsconfig.jsonandjsconfig.json. The ordering is the point:tsconfig.jsonexists it stays authoritative, and it reaches thebase itself through
extends(part 1). The fallback never takes it over.extendsfrom the rootreaches nothing: a solution-style root config —
"references": [...]withno
extendsand nopaths, which is whatnrwl/nx's own repository ships —or no root
tsconfig.jsonat all, the classic Nx integrated layout theissue describes.
aliases no longer stops the search. Previously the first readable config won
outright, so a solution-style root would short-circuit to
nulland the basewould never be reached.
AliasMapandapplyAliases()are unchanged, so callers are untouched. When theroot config declares
pathsitself, the anchor is still the project root — theprevious behaviour, byte for byte.
Tests
New
__tests__/tsconfig-extends-aliases.test.ts, 11 cases. 8 of them fail onmainand pass here; the rest are regression guards for existing behaviour(nearest-config override, root-over-base precedence, and "no
pathsanywhere inthe chain" still returning
null).Two things worth calling out about how they were written:
pathsinside the cycle, so it onlypasses if the chain is actually walked and the cycle is cut — an earlier
version with the
pathsoutside the cycle passed on unfixed code, i.e. testednothing.
and no root config), and both were confirmed red against the
extends-onlyversion of this branch before the candidate was added — so they are testing
the fallback specifically, not riding on part 1.
npm run buildand the fullnpm testsuite are green locally(164 files / 2912 tests passed, 15 files / 178 tests skipped), and
tsc --noEmitis clean.