mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
### 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]>
36 lines
1.3 KiB
TypeScript
36 lines
1.3 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
|
|
describe('turbo-resolve-extensions', () => {
|
|
const { next } = nextTestSetup({
|
|
files: __dirname,
|
|
})
|
|
|
|
it('should SSR', async () => {
|
|
const res = await next.fetch('/')
|
|
const html = await res.text()
|
|
expect(html).toContain('hello world')
|
|
expect(html).toContain('hello server')
|
|
expect(html).toContain('hello image 1')
|
|
expect(html).toContain('hello image 2')
|
|
})
|
|
|
|
it('should work using browser', async () => {
|
|
const browser = await next.browser('/')
|
|
const text = await browser.elementByCss('body').text()
|
|
expect(text).toContain('hello world')
|
|
expect(text).toContain('hello client')
|
|
})
|
|
|
|
it('should respect resolveExtensions priority (.web.tsx before .tsx)', async () => {
|
|
// When both PlatformComponent.web.tsx and PlatformComponent.tsx exist,
|
|
// the .web.tsx variant must win because it appears first in resolveExtensions.
|
|
// This guards against a Turbopack bug where the alternative index was
|
|
// hardcoded to 0 in the fast path, collapsing all alternatives to the same
|
|
// priority and making the winner non-deterministic (usually wrong).
|
|
const res = await next.fetch('/')
|
|
const html = await res.text()
|
|
expect(html).toContain('hello web platform')
|
|
expect(html).not.toContain('hello default platform')
|
|
})
|
|
})
|