fix(turbopack): preserve resolveExtensions priority in read_matches fast path (#91856)
### What?
Fix a bug where Turbopack ignored the priority order of
`resolveExtensions` when both a platform-specific variant (e.g.
`Component.web.tsx`) and a default variant (`Component.tsx`) exist for
the same module path. The first-listed extension should always win, but
it often didn't.
### Why?
Turbopack's `read_matches` function iterates over constant alternatives
extracted from a resolve pattern (one per extension in
`resolveExtensions`). It uses `enumerate()` to assign a priority `index`
to each alternative — lower index = higher priority — so the correct
file is picked after sorting.
The `until_end=true` branch (direct file existence check) correctly
threaded `index` through. But the `until_end=false` branch (subdirectory
descent, the path taken when the pattern contains a `/` before the
dynamic suffix) had a **hardcoded `0`** instead of `index`:
```rust
// before — all alternatives get the same priority
nested.push((0, read_matches(fs_path.clone(), ...)));
// after — priority matches the extension's position in resolveExtensions
nested.push((index, read_matches(fs_path.clone(), ...)));
```
With all alternatives at priority 0, the tie-breaking sort was
effectively arbitrary. In practice `Component.tsx` frequently beat
`Component.web.tsx` even when `.web.tsx` was listed first, breaking
platform-specific module overrides — a common React Native Web / Expo
Web pattern.
### How?
**One-line Rust fix** in
`turbopack/crates/turbopack-core/src/resolve/pattern.rs`: replace the
hardcoded `0` with the loop variable `index` in the `until_end=false`
branch of the fast path.
**Turbopack unit tests** (`turbopack-core`):
- `test_custom_extensions_web_before_default` — `.web.tsx` wins when
listed first in `resolveExtensions`
- `test_custom_extensions_fallback_when_web_missing` — correctly falls
back to `.tsx` when `.web.tsx` is absent
- Extended `test_read_matches` with `extension_ordering` and
`subpath_ordering` cases covering both the `until_end=true` and
`until_end=false` branches
**Next.js e2e test** (extended `test/e2e/app-dir/resolve-extensions/`):
- Added `PlatformComponent.web.tsx` (renders `"hello web platform"`) and
`PlatformComponent.tsx` (renders `"hello default platform"`)
- `.web.tsx` is placed before `.tsx` in `resolveExtensions` in
`next.config.js` (for both Turbopack and webpack)
- New assertion: SSR'd HTML must contain `"hello web platform"` and must
**not** contain `"hello default platform"`
Fixes #91117
- [x] Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
---------
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>