Skip to content

fix(resolution): load path aliases through tsconfig extends and base configs (#1534) - #1548

Open
maxmilian wants to merge 1 commit into
colbymchenry:mainfrom
maxmilian:fix/1534-tsconfig-extends
Open

fix(resolution): load path aliases through tsconfig extends and base configs (#1534)#1548
maxmilian wants to merge 1 commit into
colbymchenry:mainfrom
maxmilian:fix/1534-tsconfig-extends

Conversation

@maxmilian

Copy link
Copy Markdown
Contributor

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 root tsconfig.json / jsconfig.json's own
compilerOptions. The header comment said as much:

 *   - does NOT follow `extends` chains yet (most projects don't need it)

Nx-style monorepos declare every alias in a tsconfig.base.json. Depending on the
generation of the workspace, the root either inherits it through extends or does
not reference it at all — so those projects got null back, 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 extends chain into a single set of effective options before
building the alias map:

  • Relative and package specifiers both resolve. ./tsconfig.base,
    ./tsconfig.base.json, @tsconfig/node18, @acme/configs/tsconfig.json
    a missing .json is implied and a bare package name means its tsconfig.json,
    resolved by walking up node_modules from the referencing config.
  • Nearest config wins. A child's paths replaces the parent's rather than
    merging, matching tsc.
  • Cycles terminate. A config already on the current chain is not re-entered,
    so a extends b extends a stops instead of recursing forever (plus a depth cap).
  • paths are anchored correctly. At baseUrl when one is declared — itself
    relative 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 the
    easy thing to get wrong: an inherited "~/*": ["src/*"] living one directory
    down must not be read as root-relative.

2. Adds tsconfig.base.json as a candidate — last in the list, after
tsconfig.json and jsconfig.json. The ordering is the point:

  • When a root tsconfig.json exists it stays authoritative, and it reaches the
    base itself through extends (part 1). The fallback never takes it over.
  • The fallback exists for the layouts where following extends from the root
    reaches nothing: a solution-style root config — "references": [...] with
    no extends and no paths, which is what nrwl/nx's own repository ships —
    or no root tsconfig.json at all, the classic Nx integrated layout the
    issue describes.
  • One related change this needs: a candidate that parses but contributes no
    aliases no longer stops the search. Previously the first readable config won
    outright, so a solution-style root would short-circuit to null and the base
    would never be reached.

AliasMap and applyAliases() are unchanged, so callers are untouched. When the
root config declares paths itself, the anchor is still the project root — the
previous behaviour, byte for byte.

Tests

New __tests__/tsconfig-extends-aliases.test.ts, 11 cases. 8 of them fail on
main
and pass here; the rest are regression guards for existing behaviour
(nearest-config override, root-over-base precedence, and "no paths anywhere in
the chain" still returning null).

Two things worth calling out about how they were written:

  • The cycle test deliberately puts the paths inside the cycle, so it only
    passes if the chain is actually walked and the cycle is cut — an earlier
    version with the paths outside the cycle passed on unfixed code, i.e. tested
    nothing.
  • The two fallback tests reproduce the two real layouts (solution-style root,
    and no root config), and both were confirmed red against the extends-only
    version of this branch before the candidate was added — so they are testing
    the fallback specifically, not riding on part 1.

npm run build and the full npm test suite are green locally
(164 files / 2912 tests passed, 15 files / 178 tests skipped), and tsc --noEmit
is clean.

…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>

@codegraph-impact codegraph-impact 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.

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 loadProjectAliases keeps scanning tsconfig.json, jsconfig.json, then tsconfig.base.json until it finds paths; 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: inherited baseUrl is resolved from the declaring config and, when absent, alias targets fall back to pathsDir. That is the load-bearing behavior for nested base configs and package-provided tsconfigs.
  • Indexing surfaces that consume alias resolution — Because src/resolution/index.ts is 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.json only contains project references and whose aliases live in tsconfig.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 extends traversal; before the PR only the root file's own compilerOptions were 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 bare extends values as package specifiers and searches upward through node_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

Full report

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.

Path alias loading ignores extends — aliases in tsconfig.base.json (Nx-style monorepos) silently fail to resolve

1 participant