fix(tsconfig): support transitive project references for paths resolution#213
fix(tsconfig): support transitive project references for paths resolution#213stormslowly wants to merge 5 commits into
Conversation
When the entry tsconfig declares `references: [B]` and B declares `references: [C]`, a file inside C should resolve via C's own `paths`, matching `tsc`'s "nearest tsconfig wins" semantics and webpack's recursive `references: "auto"` walk. Previously, rspack-resolver loaded only the directly-listed references (one level deep) and `TsConfig::resolve` only iterated those direct references. As a result, requests from a transitively-referenced project's directory would silently fall back to the entry tsconfig's `paths` and resolve incorrectly. Changes: - `load_tsconfig` extracts `load_references` which recursively loads nested project references. Existing self-reference detection (A → A) is preserved; cycle detection across multiple levels is intentionally out of scope for this change. - `TsConfig::resolve` recursively descends into nested references via `find_reference_paths`, returning the nearest reference whose `base_path` contains the requested path before falling back to the current tsconfig.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 28e11ff3d9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
This PR fixes TypeScript tsconfig project reference handling so paths resolution honors transitive references (A → B → C), matching TypeScript’s “nearest tsconfig wins” behavior and enhanced-resolve’s recursive reference walk.
Changes:
- Recursively load referenced
tsconfigs so nested reference configs (and theirpaths) are available during resolution. - Update
TsConfig::resolveto recursively search nested references and use the nearest matching reference config forpaths. - Add a new fixture and test covering a 3-level transitive reference chain.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/lib.rs | Extracts and adds recursive load_references to load nested project references. |
| src/tsconfig.rs | Adds recursive reference lookup (find_reference_paths) used by TsConfig::resolve. |
| src/tests/tsconfig_project_references.rs | Adds transitive_references test validating behavior across 3 reference levels. |
| fixtures/tsconfig/cases/references-transitive/app/tsconfig.json | Adds entry project tsconfig for transitive reference fixture. |
| fixtures/tsconfig/cases/references-transitive/app/aliased/index.ts | Adds aliased target file for app in fixture. |
| fixtures/tsconfig/cases/references-transitive/project_b/tsconfig.json | Adds middle project tsconfig referencing project_c. |
| fixtures/tsconfig/cases/references-transitive/project_b/src/aliased/index.ts | Adds aliased target file for project_b in fixture. |
| fixtures/tsconfig/cases/references-transitive/project_c/tsconfig.json | Adds leaf project tsconfig defining its own paths. |
| fixtures/tsconfig/cases/references-transitive/project_c/aliased/index.ts | Adds aliased target file for project_c in fixture. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| fn find_reference_paths(&self, path: &Path, specifier: &str) -> Option<Vec<PathBuf>> { | ||
| for tsconfig in self | ||
| .references | ||
| .iter() | ||
| .filter_map(|reference| reference.tsconfig.as_ref()) | ||
| { | ||
| if let Some(nested) = tsconfig.find_reference_paths(path, specifier) { | ||
| return Some(nested); | ||
| } | ||
| if path.starts_with(tsconfig.base_path()) { | ||
| return tsconfig.resolve_path_alias(specifier); | ||
| return Some(tsconfig.resolve_path_alias(specifier)); | ||
| } |
When a project reference uses `extends` to inherit its `baseUrl`/`paths` from a shared base config, those fields must be merged before the reference is consulted for resolution. The previous flow loaded each reference through `cache.tsconfig` and only ran the extends-merging logic on the entry tsconfig, so a transitively-referenced project whose aliases live in an extended base file would resolve as if it had no aliases at all. This was a pre-existing latent bug at depth 1 — direct references with `extends` did not inherit their aliases either — that the new transitive-references support made easier to hit. Changes: - Extract `merge_tsconfig_extends` from `load_tsconfig` and call it from `load_references`' inner callback after the self-reference check, before recursing into nested references. - Update the `references-transitive` fixture: `project_c` now reads its `paths` from a sibling `tsconfig.base.json` via `extends`. The existing `transitive_references` test fails before this fix (NotFound) and passes after. Detected via Codex review of #213.
Add a dedicated test (`references_with_extends`) and fixture (`references-extends/`) that exercises the fix from the previous commit in isolation: a single-level reference whose `baseUrl`/`paths` are inherited via `extends "../tsconfig.base.json"`. Without `merge_tsconfig_extends` being applied to the reference, the test fails with `NotFound` — confirming the test would have caught the latent extends-on-reference bug. Also revert the `references-transitive` fixture so it focuses purely on multi-level walking; the extends-merging behavior is now covered by the new fixture and test.
A pair of project references that point at each other (a → b → a) previously caused infinite recursion when `references: "auto"` recursively loaded the graph, blowing the test thread's stack. Add a `visited` set threaded through `load_references`. Each tsconfig inserts its own path before walking its references. When a candidate reference's loaded path is already in the chain, the cycle edge is cut: the reference is still attached so its own `paths` are honored, but its nested references are not walked. Includes a `references-cycle` fixture (a ↔ b) and a `cyclic_references` test that asserts resolution succeeds from both sides without stack overflow. Direct self-reference (A → A) detection via the existing equality check inside the cache callback is preserved.
Summary
Make rspack-resolver's tsconfig project-reference handling match
tsc's "nearest tsconfig wins" semantics andenhanced-resolve's recursivereferences: "auto"walk.Three bugs are fixed:
references: [B]andBdeclaresreferences: [C], a file insideCshould resolve viaC's ownpaths. Previously, only directly-listed references were loaded (one level deep) andTsConfig::resolveonly iterated those, so requests from a transitively-referenced project's directory silently fell back to the entry tsconfig'spaths.extendsnot applied to referenced configs. A referenced project whosebaseUrl/pathscome fromextends "../tsconfig.base.json"resolved as if it had no aliases. Pre-existing latent bug at depth 1 that the new transitive support makes easier to hit.a → b → a) caused infinite recursion whenreferences: "auto"recursively walked the graph.Behavior comparison (file at
c/src/foo.ts, chaina → b → c, entrya/tsconfig.json)tsc(nearest tsconfig wins)c/tsconfig.json✅enhanced-resolvereferences: "auto"(recursive)c/tsconfig.json✅a/tsconfig.json❌c/tsconfig.json✅Changes
src/lib.rsload_referencesfromload_tsconfig. The helper recursively loads each reference's own references so transitivepathsare honored.merge_tsconfig_extends; call it on referenced tsconfigs (insideload_references) so a reference inherits its base config'sbaseUrl/pathsbefore its own references are walked.visited: &mut FxHashSet<PathBuf>throughload_references. Each tsconfig inserts its own path before walking its references; when a candidate reference is already in the chain, the cycle edge is cut (the reference is still attached so itspathsare honored, but its nested references are not walked). Direct self-reference (A → A) detection via the existing equality check inside the cache callback is preserved.src/tsconfig.rs:TsConfig::resolverecursively descends into nested references via a newfind_reference_pathshelper, returning the nearest reference whosebase_pathcontains the requested path before falling back to the current tsconfig's ownpaths.references-transitive/— three-level chainapp → project_b → project_c.references-extends/— single-level reference whosepathscome fromextends "../tsconfig.base.json".references-cycle/—a ↔ bpair.tsconfig_project_references:transitive_references— covers all three levels of the chain.references_with_extends— coversextendsinheritance through a reference.cyclic_references— asserts resolution succeeds from both sides of ana ↔ bcycle without stack overflow.Test plan
cargo test --all-features --lib -- --skip pnp— 125 passed (3 new tests + 122 existing, includingauto,manual,disabled,self_reference,tsconfig_file_as_file_dependencies)cargo clippy --all-features -- --deny warningscleancargo fmt --checkclean