Files
73e654f678 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 <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-03-26 11:28:06 -07:00

27 lines
433 B
JavaScript

// .web.tsx intentionally placed before .tsx to verify resolveExtensions priority
const extensions = [
'',
'.png',
'.web.tsx',
'.tsx',
'.ts',
'.jsx',
'.js',
'.json',
]
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
turbopack: {
resolveExtensions: [...extensions],
},
webpack(config) {
config.resolve.extensions = [...extensions]
return config
},
}
module.exports = nextConfig