Skip to content

fix(tsconfig): let project references take priority over their parent#1151

Merged
Boshen merged 1 commit into
mainfrom
fix-tsconfig-project-reference-priority
May 24, 2026
Merged

fix(tsconfig): let project references take priority over their parent#1151
Boshen merged 1 commit into
mainfrom
fix-tsconfig-project-reference-priority

Conversation

@Boshen

@Boshen Boshen commented May 24, 2026

Copy link
Copy Markdown
Member

Summary

Align with TypeScript's isSourceOfProjectReferenceRedirect semantics: when a parent tsconfig has references, a referenced sub-project that includes the file always wins, even when the parent's include / default **/* glob also covers it.

Previously the parent claimed ownership first and references were only consulted as a fallback. The biggest practical impact: a solution-style root (only references, no include / files) defaulted its include to **/*, claimed every file, and hid the referenced sub-project's compilerOptions.paths — exactly the bug in #1086 / rolldown/rolldown#8468.

Changes

  • src/tsconfig.rsresolve_tsconfig_solution no longer requires that the file is not included in the parent; references are checked first whenever they exist.
  • fixtures/tsconfig/cases/project-references-priority/ — minimal repro: solution-style root + referenced tsconfig.app.json with paths; verified to match tsgo / tsserver behavior.
  • src/tests/tsconfig_project_references.rs — new test referenced_paths_win_over_root_with_no_paths covering the fixture.
  • src/tests/tsconfck.rspart_of_solution's two referenced-extends-original expectations updated from the root tsconfig to the matching referenced sub-projects. Verified against TypeScript 5.5.4 tsserver (projectInfo request); tsconfck's own behavior diverges from TypeScript here and we deliberately follow TypeScript per Align tsconfig project references resolution priority with typescript #1086.

Verification

Probed all 18 part_of_solution cases against tsserver: 16/18 now match exactly (the two unrelated divergences pre-date this change and concern empty-include defaulting + solution-style fallback to inferredProject; left for follow-up).

Closes #1086. Helps rolldown/rolldown#8468.

Align with TypeScript's `isSourceOfProjectReferenceRedirect`: when a
parent tsconfig has `references`, a referenced sub-project that includes
the file always wins, even when the parent's `include` / default
`**/*` also covers it. Previously the parent claimed ownership first
and references were only consulted as a fallback, so a solution-style
root (only `references`, no `include` / `files`) hid the referenced
sub-project's `compilerOptions.paths`.

Closes #1086.
@codecov

codecov Bot commented May 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.14%. Comparing base (798cf49) to head (84ca374).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1151      +/-   ##
==========================================
- Coverage   93.14%   93.14%   -0.01%     
==========================================
  Files          22       22              
  Lines        4159     4158       -1     
==========================================
- Hits         3874     3873       -1     
  Misses        285      285              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq

codspeed-hq Bot commented May 24, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 13 untouched benchmarks
⏩ 5 skipped benchmarks1


Comparing fix-tsconfig-project-reference-priority (84ca374) with main (798cf49)

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@Boshen

Boshen commented May 24, 2026

Copy link
Copy Markdown
Member Author

Verification — broader probe across 15 scenarios

I built a side-by-side probe comparing tsserver (TypeScript 5.5 and 6.0) with our find_tsconfig. Each scenario hand-crafts a minimal tsconfig tree and asks "which config owns this file?".

# Scenario tsserver 5.5 tsserver 6.0 oxc-resolver
01 solution root, ref includes file (the bug) pkg-a pkg-a pkg-a
02 solution root, ref misses file root root root ✓
03 parent.paths vs ref.paths, file in both pkg-a pkg-a pkg-a
04 multi-ref overlap (both claim file) pkg-b ⚠️ pkg-a pkg-a
05 multi-ref, only second matches pkg-b pkg-b pkg-b
06a ref.files matches pkg-a pkg-a pkg-a
06b ref.files misses sibling INFERRED root pkg-a
07a ref include w/ exclude (included) pkg-a pkg-a pkg-a
07b ref exclude actually excluded INFERRED root pkg-a
08a ref wins over parent narrow include pkg-a pkg-a pkg-a
08b parent owns when ref misses root root root ✓
09 nested ref (transitive) pkg-a/pkg-b pkg-a/pkg-b pkg-a/pkg-b
10 ref without composite pkg-a pkg-a pkg-a
11 solution explicit files: [] pkg-a pkg-a pkg-a
12 file in both parent.files and ref.include pkg-a pkg-a pkg-a

Result: 13 / 15 agree with TypeScript 6.0 (current stable).

  • Case 04 confirms TS 6.0 changed to "first reference in declaration order wins" (TS 5.5 had picked the last); our implementation already matches the modern algorithm.
  • Cases 06b and 07b are pre-existing divergences not caused by this PR — they're about discovery (we walk up from the file and stop at the nearest tsconfig, missing the solution root above), not the reference-priority logic this PR fixes. Behaviorally harmless when the inner tsconfig has no paths/baseUrl. Worth a follow-up.

Confirmed against typescript-go's source

internal/project/projectcollection.go:202 findDefaultConfiguredProjectWorker does a BFS over the project + its references and stops at the first one that contains the file and is not redirecting it — exactly the "reference wins" rule this PR implements.

@Boshen Boshen merged commit ac7a26b into main May 24, 2026
18 checks passed
@Boshen Boshen deleted the fix-tsconfig-project-reference-priority branch May 24, 2026 14:52
@oxc-guard oxc-guard Bot mentioned this pull request May 24, 2026
Boshen added a commit that referenced this pull request May 25, 2026
## 🤖 New release

* `oxc_resolver`: 11.19.1 -> 11.19.2
* `oxc_resolver_napi`: 11.19.1 -> 11.19.2

<details><summary><i><b>Changelog</b></i></summary><p>

## `oxc_resolver`

<blockquote>

##
[11.19.2](v11.19.1...v11.19.2)
- 2026-05-25

### <!-- 1 -->🐛 Bug Fixes

- *(tsconfig)* apply later-wins semantics for extends array
([#1156](#1156)) (by
@Boshen)
- *(tsconfig)* walk past a tsconfig that doesn't claim the file
([#1154](#1154)) (by
@Boshen)
- *(tsconfig)* let project references take priority over their parent
([#1151](#1151)) (by
@Boshen)
- *(tsconfig)* resolve `rootDirs` against the config that declared them
([#1150](#1150)) (by
@Boshen)
- *(tsconfig)* resolve `baseUrl` / `paths` against the canonical
tsconfig path
([#1148](#1148)) (by
@Boshen)
- strip query fragments when calling `find_tsconfig`
([#1147](#1147)) (by
@Boshen)
- avoid panic in resolve_file for parentless paths
([#1053](#1053)) (by
@Boshen)
- *(dts)* strip ./ prefix from package entry when matching typesVersions
([#1051](#1051)) (by
@Boshen)
- *(dts)* expand Declaration to TypeScript|Declaration for package entry
resolution
([#1050](#1050)) (by
@Boshen)
- *(dts)* prefer declaration extensions over JS in exports-resolved
paths ([#1047](#1047))
(by @Boshen)
- avoid wasm/wasi dead-code lint in NodePath
([#1043](#1043)) (by
@Boshen)
- *(napi)* replace panics with error returns to prevent WASM traps
([#1055](#1055)) (by
@Boshen)

### <!-- 2 -->🚜 Refactor

- remove clear_cache test that dynamically creates fixtures (by @Boshen)
- move resolve and misc fixtures into fixtures/integration (by @Boshen)
- replace ignored doctest with link to example (by @Boshen)
- consolidate fixture directories for better test file mapping (by
@Boshen)
- replace `url` crate with `percent-encoding`
([#1065](#1065)) (by
@Boshen)

### <!-- 4 -->⚡ Performance

- *(cache)* pack CachedPathImpl::meta into a CachedMeta byte
([#1144](#1144)) (by
@Boshen)
- *(cache)* store canonical path as Box<Path> instead of PathBuf
([#1143](#1143)) (by
@Boshen)
- *(alias)* fast-reject alias entries by cached first byte
([#1142](#1142)) (by
@Boshen)

### <!-- 6 -->🧪 Testing

- *(tsconfig)* port lookup scenarios from typescript-go
([#1155](#1155)) (by
@Boshen)
- add 28 tests to improve coverage (92% → 93%)
([#1082](#1082)) (by
@Boshen)

### Contributors

* @Boshen
* @renovate[bot]
</blockquote>



</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).

Co-authored-by: oxc-guard[bot] <276638029+oxc-guard[bot]@users.noreply.github.com>
Boshen added a commit that referenced this pull request May 27, 2026
…#1161)

## Summary

The default `include = **/*` was matching every path regardless of where
the tsconfig sat, so a referenced sub-project with no `files`/`include`
of its own claimed files *outside* its own directory and shadowed the
parent's `compilerOptions.paths`. Combined with the solution-style
heuristic treating omitted `files`/`include` the same as explicit `[]`,
a root tsconfig with `references` and `paths` but no `include` lost its
own paths for files it owns via the default glob.

## Repro

```
repro/
├── tsconfig.json        compilerOptions: { paths: { "@app/*": ["./src/*"] } }, references: [{ path: "./pkg" }]
├── index.ts             import { util } from "@app/util";
├── src/util.ts
└── pkg/
    ├── tsconfig.json    compilerOptions: { composite: true }
    └── thing.ts
```

`tsc 6.0.3` and `tsgo 7.0.0-dev` both resolve `@app/util` →
`src/util.ts`. oxc-resolver 11.19.1 did too; 11.19.2 returns `Cannot
find module`.

## Root cause

Two interacting bugs:

1. `is_glob_matches(_, GlobPattern::All)` short-circuited `**/*` to
`true` regardless of which tsconfig owned the pattern. Fine when the
only caller was the walked-up parent tsconfig, broken after #1151
started asking *references* the same question — their `**/*` then
matched files outside the reference's directory.
2. `claims_ownership_of`'s solution-style check used
`is_none_or(Vec::is_empty)`, conflating omitted and explicit-empty
`files`/`include`. Per the TS spec only an explicit `[]` means "own no
files"; an omitted `include` defaults to `**/*` and should fall through.

This mirrors typescript-go's structure: `getFileNamesFromConfigSpecs`
expands the default `**/*` via `ReadDirectory(basePath, ...)`, which is
naturally scoped to the project directory — no equivalent "literal
`**/*` matches everywhere" code path exists there.

## Changes

- `src/tsconfig.rs` — `GlobPattern::All` now checks
`path.starts_with(self.directory())`; `is_solution_style` requires
explicit `Some([])` for both `files` and `include`. Dead
`GLOB_ALL_PATTERN` constant removed.
- `src/tests/tsconfig_project_references.rs` —
`root_paths_apply_to_default_include_files` covers the repro.
- `fixtures/tsconfig/cases/project-references-default-include/` —
minimal fixture matching the issue.

All 294 existing tests still pass.

Closes #1159.
shulaoda added a commit to rolldown/rolldown that referenced this pull request Jun 3, 2026
> [!IMPORTANT]
> **This is a minor release.** Two changes alter default behavior compared to `1.0.3`. Please read this section before upgrading. Everything else is additive (new features, fixes, deps).

## ⚠️ Notable behavior changes

### 1. `experimental.lazyBarrel` is now enabled by default (#9632)

**What changed.** `experimental.lazyBarrel` now defaults to `true`. When a barrel module is recognized as side-effect-free, Rolldown skips compiling the re-exported modules that are never actually used.

**Impact.** For codebases with large barrel files (component libraries such as Ant Design, `@mui/icons-material`, etc.) this is a meaningful build-time speedup, and for the vast majority of projects the emitted output is unchanged. In rare cases where a barrel is *incorrectly* treated as side-effect-free, the optimization could drop a module that was being relied on for its side effects.

**How to opt out (backward compatible).**

```js
// rolldown.config.js
export default {
  experimental: { lazyBarrel: false },
}
```

> Note: this opt-out flag is planned to be removed in a future release. If you have a case where you must turn it off, please open an issue so we can fix the underlying detection instead.

---

### 2. `tsconfig` project-reference resolution now aligns with TypeScript

Upgrading `oxc_resolver` (`11.19.1` → `11.20.0` in #9549, then `→ 11.21.0` in #9634) changes how a *solution-style* `tsconfig.json` (one that only lists `references` and delegates the real settings to `tsconfig.app.json` / `tsconfig.node.json`, as Vite scaffolds) is resolved, bringing it **in line with how TypeScript (`tsc`) itself behaves**:

- **Reference match priority** (oxc-resolver [#1151](oxc-project/oxc-resolver#1151)): when the root has `references`, a referenced project that includes the file now **takes precedence over the root**, instead of the root matching it first (this is what TypeScript already does). So that project's `compilerOptions.paths` now apply.
- **`allowJs`** (oxc-resolver [#1198](oxc-project/oxc-resolver#1198)): whether a `.js`/`.jsx`/`.mjs`/`.cjs` file is included is now decided by **each referenced project's own** `allowJs`, not the root's (again matching TypeScript). So `tsconfig.app.json` with `allowJs: true` + `paths` now resolves aliases for `.js` files even when the root doesn't set `allowJs`.

For most projects this is a fix (the standard Vite `paths` aliases now resolve, closes #8468), but it **is** a behavior change if you relied on the previous behavior, where the root's `paths` / `allowJs` took precedence.

**If you relied on the old "root wins" behavior.** There is no exact toggle back, because the old behavior was the bug being fixed. The recommended path is to align your config with TypeScript: declare the `paths` / `allowJs` on the referenced project that actually owns the files.

If you must keep the old precedence while still using `references`: a referenced project's match wins, and **the first matching `references` entry takes priority** (the root is only a fallback when no reference claims the file). So extract the old root settings into their own config and list it **first**:

```jsonc
// tsconfig.json (solution root)
{
  "files": [],
  "references": [
    { "path": "./tsconfig.base.json" }, // old root paths/allowJs — listed first, so it wins
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]
}
```

`tsconfig.base.json` should carry the `paths` you previously declared on the root, plus `allowJs: true` if it needs to claim `.js` files (the extension is checked against each config's own `allowJs`). With no `include`, it defaults to `**/*` under its directory and claims every file first.

Alternatively, bypass reference resolution entirely by pointing the top-level `tsconfig` option at a single config: `export default { tsconfig: './tsconfig.app.json' }`.

---

## [1.1.0] - 2026-06-03

### 🚀 Features

- enable `experimental.lazyBarrel` by default (#9632) by @shulaoda
- `import.meta.glob` support `caseSensitive` option (#9594) by @btea
- add `SOURCEMAP_BROKEN` warning for renderChunk hook (#9601) by @sapphi-red
- add `SOURCEMAP_BROKEN` warning for transform hook (#9600) by @sapphi-red
- add `@__NO_SIDE_EFFECTS__` hint for invalid `@__PURE__` before function declarations (#9505) by @Copilot
- code-splitting: support group-local `includeDependenciesRecursively` (#9587) by @hyf0

### 🐛 Bug Fixes

- report TSCONFIG_ERROR instead of UNHANDLEABLE_ERROR for a missing tsconfig file (#9633) by @shulaoda
- browser: add missing exports and ensure consistency with `rolldown` package (#9629) by @sapphi-red
- should build test-dev-server when test-node (#9610) by @situ2001
- chunk-optimizer: refuse asymmetric merge for cyclic dynamic entries (#9320) (#9322) by @aminpaks
- dev: handle the remaining errors in dev (#9570) by @h-a-n-a
- handle slash-normalized ids with preserveModulesRoot (#9595) by @IWANABETHATGUY
- json: preserve .default access on JSON default imports (#9568) by @IWANABETHATGUY
- testing: remove unintended trigger_full_build from test harness (#9573) by @hyf0

### 🚜 Refactor

- js-regex: use regress native replace/replace_all (#9607) by @IWANABETHATGUY
- remove never-constructed `ImportStatus` variants (#9606) by @Boshen

### 📚 Documentation

- clarify that `RolldownBuild::close` method should be called in most cases (#9619) by @sapphi-red

### ⚡ Performance

- avoid unnecessary intermediate sourcemaps (#9599) by @sapphi-red

### 🧪 Testing

- add unit test for collapsing module sourcemap (#9626) by @sapphi-red
- cover vite-alias regex capture-group expansion (#9602) (#9608) by @IWANABETHATGUY

### ⚙️ Miscellaneous Tasks

- deps: update oxc_resolver to 11.21.0 (#9634) by @shulaoda
- update invalid option diagnostic link to point to Rolldown docs (#9631) by @sapphi-red
- deps: update vite+ to v0.1.24 (#9628) by @renovate[bot]
- deps: update oxc resolver to v11.20.0 (#9549) by @renovate[bot]
- deps: update dependency vite-plus to v0.1.24 (#9470) by @renovate[bot]
- deps: update npm packages (#9614) by @renovate[bot]
- deps: upgrade oxc to 0.134.0 (#9625) by @shulaoda
- deps: update crate-ci/typos action to v1.47.0 (#9620) by @renovate[bot]
- deps: update rollup submodule for tests to v4.61.0 (#9623) by @rolldown-guard[bot]
- deps: update github actions (#9613) by @renovate[bot]
- deps: update pnpm to v11.4.0 (#9616) by @renovate[bot]
- deps: update rust crates (#9615) by @renovate[bot]
- deps: update test262 submodule for tests (#9624) by @rolldown-guard[bot]
- deps: update dependency @napi-rs/cli to v3.7.0 (#9588) by @renovate[bot]
- deps: update dependency rust to v1.96.0 (#9596) by @renovate[bot]
- re-enable WASI testing with proper infrastructure (#9397) by @Boshen

### ❤️ New Contributors

* @aminpaks made their first contribution in [#9322](#9322)

Co-authored-by: shulaoda <165626830+shulaoda@users.noreply.github.com>
shulaoda added a commit to rolldown/rolldown that referenced this pull request Jun 3, 2026
…ior (#9641)

## What

Updates `packages/rolldown/src/options/docs/tsconfig.md` to describe how a tsconfig's `references` are resolved as of **v1.1.0**. Docs-only, no behavior or code change.

## Why

The reference resolution behavior changed in v1.1.0 through the `oxc_resolver` upgrades:

- **#9549** (`oxc_resolver` → `11.20.0`) brought in oxc-resolver [#1151](oxc-project/oxc-resolver#1151): a referenced project that includes the file takes precedence over the root, instead of the root claiming it first.
- **#9634** (`oxc_resolver` → `11.21.0`) brought in oxc-resolver [#1198](oxc-project/oxc-resolver#1198): whether a `.js`/`.jsx`/`.mjs`/`.cjs` file is included is decided by each referenced project's own `allowJs`, not the root's.

The existing docs still described the previous model (root matches first, references consulted only as a fallback), which no longer holds. A solution-style `tsconfig.json` (only `references`, as Vite scaffolds) now resolves the way TypeScript does, which is also what made the standard Vite `paths` aliases resolve correctly again (#8468).

## Changes

- Rewrote the `references` resolution paragraph: a referenced project that includes the file takes precedence over the root, each referenced project uses its own `allowJs` (so a `.js`/`.jsx`/`.mjs`/`.cjs` file is only included where that project enables it), and the root is used only when no referenced project includes the file.
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.

Align tsconfig project references resolution priority with typescript

1 participant