Describe the bug
AI disclosure:
I used an AI assistant to isolate this down to the minimal reproduction and to review this description.
The bug was first observed in a real codebase; the reproduction is human-reviewed and CI-verified.
On Windows, a production build duplicates a module when one physical file is imported through both a relative specifier and a tsconfig.json paths alias. This is NOT the "two different files" case (cf. #22351); it is a single file that resolves to two module ids differing only by slash direction:
- relative
./shared -> C:/dev/.../src/shared/index.js (forward slashes)
- alias
@shared/index -> C:\dev\...\src\shared\index.js (back slashes)
Rolldown keys its module graph on the raw id string, so it treats these as two separate modules and emits two copies. The proof that it's the same file (and a normalization bug, not intended behavior) is the platform asymmetry: on Linux both specifiers resolve to one forward-slash id and the module is bundled once; only on Windows is it duplicated. Any module-level singleton is therefore duplicated on Windows. In the real app where I hit this, the duplicated module held a React.createContext, so the Provider and a useContext bound to different context objects and the app threw "must be used within a Provider", at runtime, from a default build, only on Windows.
This happens with no plugins and the default config. The alias is resolved by Vite/Rolldown's native tsconfig.json paths handling, active at the default resolve.tsconfigPaths: false. I isolated it:
- Reproduces with zero Vite plugins and no manual
resolve.alias. Renaming tsconfig.json so it can't be found makes the build fail with Rolldown failed to resolve import "@shared/index", confirming the tsconfig-paths resolver is what resolves it.
- Vite's own
resolve.alias is not affected; it normalizes to a single forward-slash id (no duplication).
- Setting
resolve.tsconfigPaths: true works around it (resolution then routes through Vite's normalized handling -> one forward-slash id -> one module). So the buggy state is the default false.
Where the fix likely belongs: Vite's tsconfig-paths resolution should normalize the resolved id (e.g. via normalizePath) before returning it. Rolldown declined to normalize ids internally (rolldown/rolldown#2303, closed wontfix), so the normalization has to happen on the Vite side. This looks like another instance of the Windows path-normalization family also seen in #15153 (duplicate module from a relative rollupOptions.input) and #22264 (backslash-vs-POSIX mismatch breaking HMR).
Expected: one physical file resolves to one canonical module id whether reached via a relative path or a tsconfig paths alias, so it's bundled once (as it already is on Linux).
Actual: two ids (back-slash vs forward-slash) on Windows -> bundled twice -> duplicated module-level singleton.
I'm not planning to open a PR myself, but happy to test a fix.
Reproduction
https://github.com/caillou/vite-windows-alias-duplication
Steps to reproduce
The repro is a single file src/main.js that imports src/shared/index.js via both ./shared and the @shared/* tsconfig path. src/shared/index.js exports const TOKEN = Symbol('shared-singleton'); main.js asserts the two imports are ===.
On Windows:
pnpm install
pnpm run repro # = vite build && node dist/index.js
- Actual (Windows): the build reports an extra module;
node dist/index.js prints DUPLICATED and exits 1 (the two TOKEN imports are not ===). dist/index.js contains Symbol('shared-singleton') twice.
- Expected / on Linux: prints
OK, exits 0, single module instance.
The repo's GitHub Actions matrix runs this on ubuntu-latest (green, OK, 3 modules) and windows-latest (red, DUPLICATED, 4 modules) on every push.
System Info
System:
OS: Windows 11 10.0.26200
CPU: (16) x64 INTEL(R) XEON(R) PLATINUM 8573C
Memory: 44.85 GB / 63.99 GB
Binaries:
Node: 24.15.0 - C:\nvm4w\nodejs\node.EXE
pnpm: 10.33.0 - C:\nvm4w\nodejs\pnpm.CMD
Browsers:
Edge: Chromium (148.0.3967.54)
npmPackages:
vite: 8.0.10 => 8.0.10
rolldown: 1.0.0-rc.17 (transitive of vite; not listed by envinfo)
Used Package Manager
pnpm
Logs
Click to expand!
# the two ids Rolldown sees for the same file src/shared/index.js (captured with a load() hook):
"C:/dev/vite-windows-alias-duplication/src/shared/index.js" <- relative ./shared
"C:\dev\vite-windows-alias-duplication\src\shared\index.js" <- alias @shared/index (tsconfig paths)
# vite build: 4 modules transformed on Windows (duplicated) vs 3 on Linux
Validations
Describe the bug
On Windows, a production build duplicates a module when one physical file is imported through both a relative specifier and a
tsconfig.jsonpathsalias. This is NOT the "two different files" case (cf. #22351); it is a single file that resolves to two module ids differing only by slash direction:./shared->C:/dev/.../src/shared/index.js(forward slashes)@shared/index->C:\dev\...\src\shared\index.js(back slashes)Rolldown keys its module graph on the raw id string, so it treats these as two separate modules and emits two copies. The proof that it's the same file (and a normalization bug, not intended behavior) is the platform asymmetry: on Linux both specifiers resolve to one forward-slash id and the module is bundled once; only on Windows is it duplicated. Any module-level singleton is therefore duplicated on Windows. In the real app where I hit this, the duplicated module held a
React.createContext, so the Provider and auseContextbound to different context objects and the app threw "must be used within a Provider", at runtime, from a default build, only on Windows.This happens with no plugins and the default config. The alias is resolved by Vite/Rolldown's native
tsconfig.jsonpathshandling, active at the defaultresolve.tsconfigPaths: false. I isolated it:resolve.alias. Renamingtsconfig.jsonso it can't be found makes the build fail withRolldown failed to resolve import "@shared/index", confirming the tsconfig-pathsresolver is what resolves it.resolve.aliasis not affected; it normalizes to a single forward-slash id (no duplication).resolve.tsconfigPaths: trueworks around it (resolution then routes through Vite's normalized handling->one forward-slash id->one module). So the buggy state is the defaultfalse.Where the fix likely belongs: Vite's tsconfig-
pathsresolution should normalize the resolved id (e.g. vianormalizePath) before returning it. Rolldown declined to normalize ids internally (rolldown/rolldown#2303, closed wontfix), so the normalization has to happen on the Vite side. This looks like another instance of the Windows path-normalization family also seen in #15153 (duplicate module from a relativerollupOptions.input) and #22264 (backslash-vs-POSIX mismatch breaking HMR).Expected: one physical file resolves to one canonical module id whether reached via a relative path or a tsconfig
pathsalias, so it's bundled once (as it already is on Linux).Actual: two ids (back-slash vs forward-slash) on Windows
->bundled twice->duplicated module-level singleton.I'm not planning to open a PR myself, but happy to test a fix.
Reproduction
https://github.com/caillou/vite-windows-alias-duplication
Steps to reproduce
The repro is a single file
src/main.jsthat importssrc/shared/index.jsvia both./sharedand the@shared/*tsconfig path.src/shared/index.jsexportsconst TOKEN = Symbol('shared-singleton');main.jsasserts the two imports are===.On Windows:
pnpm install pnpm run repro # = vite build && node dist/index.jsnode dist/index.jsprintsDUPLICATEDand exits 1 (the twoTOKENimports are not===).dist/index.jscontainsSymbol('shared-singleton')twice.OK, exits 0, single module instance.The repo's GitHub Actions matrix runs this on
ubuntu-latest(green,OK, 3 modules) andwindows-latest(red,DUPLICATED, 4 modules) on every push.System Info
System: OS: Windows 11 10.0.26200 CPU: (16) x64 INTEL(R) XEON(R) PLATINUM 8573C Memory: 44.85 GB / 63.99 GB Binaries: Node: 24.15.0 - C:\nvm4w\nodejs\node.EXE pnpm: 10.33.0 - C:\nvm4w\nodejs\pnpm.CMD Browsers: Edge: Chromium (148.0.3967.54) npmPackages: vite: 8.0.10 => 8.0.10 rolldown: 1.0.0-rc.17 (transitive of vite; not listed by envinfo)Used Package Manager
pnpm
Logs
Click to expand!
Validations