fix(vite): correct tsconfig paths fallback resolution - #36424
Open
leosvelperez wants to merge 10 commits into
Open
fix(vite): correct tsconfig paths fallback resolution#36424leosvelperez wants to merge 10 commits into
leosvelperez wants to merge 10 commits into
Conversation
✅ Deploy Preview for nx-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for nx-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Contributor
|
View your CI Pipeline Execution ↗ for commit 17f0492
☁️ Nx Cloud last updated this comment at |
leosvelperez
marked this pull request as ready for review
July 21, 2026 11:42
leosvelperez
force-pushed
the
gh-36400
branch
2 times, most recently
from
August 6, 2026 12:41
8e8c94c to
797ed44
Compare
Contributor
There was a problem hiding this comment.
Nx Cloud has identified a flaky task in your failed CI:
🔂 Since the failure was identified as flaky, we triggered a CI rerun by adding an empty commit to this branch.
🔔 Heads up, your workspace has pending recommendations ↗ to auto-apply fixes for similar failures.
🎓 Learn more about Self-Healing CI on nx.dev
…iases The fallback resolver used when `tsconfig-paths` cannot match an import dropped everything a mapped path placed after the `*`, so an alias mapped to `packages/*/src` or `packages/foo/*.ts` never resolved. It also ran the import through a string replacement, which expanded `$&` and friends as substitution patterns. The captured suffix is now substituted into the wildcard through function replacements. Retrying without the import extension is limited to mapped paths whose wildcard is last: anything a mapping appends after the `*` is not the import's own tail, and slicing it off resolves a sibling the mapping never pointed at. `configResolved` also assumed a workspace always has a root-level tsconfig. Without one, `loadConfig` searched upwards from the cwd and either picked up an unrelated tsconfig outside the workspace, throwing during config resolution, or left the fallback unset and threw on every unresolvable import. Both cases now defer to Vite's own resolution.
Walking the `extends` chain to find the tsconfig that declares `paths` parsed each file with `JSON.parse` inside a silent try/catch. A single comment or trailing comma anywhere in the chain dropped that file and everything it extends, so the baseUrl fell back to the directory of the leaf tsconfig and every alias resolved under a directory that does not exist. Reading through `readJsonFile` accepts the same JSONC TypeScript does.
Route TempFs through the @nx/devkit/internal-testing-utils subpath export, matching the other plugin specs. The bare nx/src import tripped the devkit import-boundary lint rule.
`loadConfig` from `tsconfig-paths` derives `absoluteBaseUrl` from the leaf tsconfig it was handed, but `paths` values resolve against the config that declared them. The fallback resolver read that leaf-derived value, so a project tsconfig inheriting `paths` through `extends` resolved every mapped path against the project directory instead of the declaring one. The primary matcher already corrected the base with `resolvePathsBaseUrl`. Hand the same value to the fallback, as the expo and react-native metro resolvers do. A workspace with a root-level `tsconfig.base.json` masks this: the second fallback pass loads that config, whose directory is the workspace root. The added test therefore names the root config `tsconfig.json`, which the root-level lookup skips in favour of the project tsconfig.
`joinPathFragments` runs its result through `normalizePath`, which strips a leading drive letter and rewrites backslashes. Its own doc comment says the output is not meant for reading files off disk. The candidates built here go straight to `existsSync`, so on Windows a workspace on a drive other than the one holding `process.cwd()` lost the drive and probed the wrong volume. `join` is used rather than `resolve` so a drive-absolute mapping value keeps being treated as it is today. No test: on POSIX the two produce identical output for the path shapes the resolver builds, so nothing here can go red on a POSIX runner.
When more than one alias matched an import, the fallback resolver took
whichever the tsconfig declared first. TypeScript instead picks by
specificity, and does so regardless of declaration order: an exact hit on a
non-wildcard alias wins outright, then the longest wildcard prefix.
With `{ "@repo/*": ["generic/*"], "@repo/exact": ["packages/exact"] }` and an
import of `@repo/exact`, the resolver returned `generic/exact.ts` while
TypeScript, checked here in all three module resolution modes, returns
`packages/exact/index.ts`. Reversing the two declarations flipped the
resolver's answer and left TypeScript's unchanged.
This orders only the alias. Falling through the values of a single alias is
what TypeScript does too, so the inner loop is untouched.
The resolver also runs when `tsconfig-paths` did match an alias but the file it pointed at is missing, which the caller decides with `existsSync`.
The unit spec pins the substitution itself. This drives it through `configResolved` and `resolveId`, so the mapped path reaching the resolver is the one the plugin actually builds.
… paths The injected `existsSync` was backed by a set of POSIX literals, so on Windows every lookup missed the `resolve`d, drive-prefixed path the resolver actually probes and the whole suite failed.
Ordering by prefix length also reordered two non-wildcard aliases that both
matched only a prefix of the import, which is a shape TypeScript does not
resolve at all:
{ "@repo": ["packages/broad"], "@repo/exact": ["packages/narrow"] }
For `@repo/exact/thing` that flipped the answer from
`packages/broad/exact/thing.ts` to `packages/narrow/thing.ts` while TypeScript,
checked in all three module resolution modes, resolves neither. The tiebreak
exists to implement TypeScript's longest-wildcard-prefix rule, so it now
applies only where TypeScript applies it and everything else keeps the order
it was declared in.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Current Behavior
The
nxViteTsPathsplugin looks up aliases in two passes. The first askstsconfig-paths. It checks only for.js,.jsonand.nodefiles. Most aliases miss it. They fall to a second pass nx runs itself. That pass has five bugs:*is not last in the mapped path."@app/*": ["packages/*/src"]never resolves. Vite gets a plain name instead.$&,$`or$'gets mangled before the lookup.pathsthroughextends. nx reads them against the project folder, not the folder that set them. Every alias points at a missing file.A workspace may have no root-level tsconfig. nx searches upward from the current folder. It finds a tsconfig outside the workspace, or throws.
nx also reads the
extendschain as strict JSON to find the file that setspaths. One comment or stray comma drops that file. Its aliases point at the wrong folder. Users hit this through the@nx/vitestexecutor.Expected Behavior
Aliases resolve wherever the
*sits in the mapped path."@app/*": ["packages/*/src"]findspackages/one/src/index.ts. An import holding a$sequence finds the file it names.A project that inherits
pathsreads them against the folder that set them. When two aliases match, nx picks the one TypeScript picks. The order they are listed in stops mattering. On Windows the drive letter survives.A workspace with no root-level tsconfig hands the import back to Vite. A project-level tsconfig still works on its own.
nx walks the
extendschain the way TypeScript does. Comments and stray commas leave aliases pointing where the tsconfig says.Related Issue(s)
Fixes #36400
Implementation Notes
ts.resolveModuleName. The check spans mapped-path shapes, import extensions and three module resolution modes. Wildcard substitution and the choice of alias agree with TypeScript in every case measured."/*"key. Workspaces build on all three today, so tightening them waits for a major.View session information ↗