diff --git a/packages/router-core/src/new-process-route-tree.ts b/packages/router-core/src/new-process-route-tree.ts index 2d3b41cc11..bbdbde56e2 100644 --- a/packages/router-core/src/new-process-route-tree.ts +++ b/packages/router-core/src/new-process-route-tree.ts @@ -1018,6 +1018,12 @@ function getNodeMatch( } } + if (bestMatch && wildcardMatch) { + return isFrameMoreSpecific(wildcardMatch, bestMatch) + ? bestMatch + : wildcardMatch + } + if (bestMatch) return bestMatch if (wildcardMatch) return wildcardMatch diff --git a/packages/router-core/tests/new-process-route-tree.test.ts b/packages/router-core/tests/new-process-route-tree.test.ts index ced5ac1b8b..65b7da14ca 100644 --- a/packages/router-core/tests/new-process-route-tree.test.ts +++ b/packages/router-core/tests/new-process-route-tree.test.ts @@ -546,6 +546,30 @@ describe('findRouteMatch', () => { expect(res?.route.id).toBe('/a/b/$') expect(res?.params).toEqual({ _splat: 'foo', '*': 'foo' }) }) + describe('edge-case #5969: trailing empty wildcard should match', () => { + it('basic', () => { + const tree = makeTree(['/a/$']) + expect(findRouteMatch('/a/', tree)?.route.id).toBe('/a/$') + expect(findRouteMatch('/a', tree)?.route.id).toBe('/a/$') + }) + it('with layout route', () => { + const tree = makeTree(['/a', '/a/$']) + expect(findRouteMatch('/a/', tree)?.route.id).toBe('/a/$') + expect(findRouteMatch('/a', tree)?.route.id).toBe('/a/$') + }) + it('with index route (should not match)', () => { + const tree = makeTree(['/a/', '/a/$']) + expect(findRouteMatch('/a/', tree)?.route.id).toBe('/a/') + expect(findRouteMatch('/a', tree)?.route.id).toBe('/a/') + }) + it('edge-case: deeper index route through skipped optional segments (should not match)', () => { + const tree = makeTree(['/{-$foo}/{-$bar}/a/', '/a/$']) + expect(findRouteMatch('/a/', tree)?.route.id).toBe( + '/{-$foo}/{-$bar}/a/', + ) + expect(findRouteMatch('/a', tree)?.route.id).toBe('/{-$foo}/{-$bar}/a/') + }) + }) }) describe('nested routes', () => {