Skip to content

Windows: a file imported via both a tsconfig paths alias and a relative path is bundled twice (non-normalized back-slash module id) #22506

@caillou

Description

@caillou

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions