diff --git a/e2e/react-router/basic-file-based/src/routes/params-ps/non-nested/route.tsx b/e2e/react-router/basic-file-based/src/routes/params-ps/non-nested/route.tsx index 3760bde7088..93fd9362182 100644 --- a/e2e/react-router/basic-file-based/src/routes/params-ps/non-nested/route.tsx +++ b/e2e/react-router/basic-file-based/src/routes/params-ps/non-nested/route.tsx @@ -16,7 +16,15 @@ function RouteComponent() { to="./$foo/$bar" params={{ foo: 'foo', bar: 'bar' }} > - /params-ps/non-nested/$foo/$bar + /params-ps/non-nested/foo/bar + + + /params-ps/non-nested/foo2/bar2 diff --git a/e2e/react-router/basic-file-based/tests/params.spec.ts b/e2e/react-router/basic-file-based/tests/params.spec.ts index d4d6d2e602b..76f32a935a8 100644 --- a/e2e/react-router/basic-file-based/tests/params.spec.ts +++ b/e2e/react-router/basic-file-based/tests/params.spec.ts @@ -69,8 +69,9 @@ test.describe('params operations + non-nested routes', () => { 'href', '/params-ps/non-nested/foo/bar', ) + await fooBarLink.click() - await page.waitForLoadState('networkidle') + await page.waitForURL('/params-ps/non-nested/foo/bar') const pagePathname = new URL(page.url()).pathname expect(pagePathname).toBe('/params-ps/non-nested/foo/bar') @@ -83,6 +84,27 @@ test.describe('params operations + non-nested routes', () => { const paramsText = await paramsValue.innerText() const paramsObj = JSON.parse(paramsText) expect(paramsObj).toEqual({ foo: 'foo', bar: 'bar' }) + + const foo2Bar2Link = page.getByTestId('l-to-non-nested-foo2-bar2') + + await expect(foo2Bar2Link).toHaveAttribute( + 'href', + '/params-ps/non-nested/foo2/bar2', + ) + await foo2Bar2Link.click() + await page.waitForURL('/params-ps/non-nested/foo2/bar2') + const pagePathname2 = new URL(page.url()).pathname + expect(pagePathname2).toBe('/params-ps/non-nested/foo2/bar2') + + const foo2ParamsValue = page.getByTestId('foo-params-value') + const foo2ParamsText = await foo2ParamsValue.innerText() + const foo2ParamsObj = JSON.parse(foo2ParamsText) + expect(foo2ParamsObj).toEqual({ foo: 'foo2' }) + + const params2Value = page.getByTestId('foo-bar-params-value') + const params2Text = await params2Value.innerText() + const params2Obj = JSON.parse(params2Text) + expect(params2Obj).toEqual({ foo: 'foo2', bar: 'bar2' }) }) }) diff --git a/e2e/solid-router/basic-file-based/src/routes/params-ps/non-nested/route.tsx b/e2e/solid-router/basic-file-based/src/routes/params-ps/non-nested/route.tsx index 496542c6677..6de7393593a 100644 --- a/e2e/solid-router/basic-file-based/src/routes/params-ps/non-nested/route.tsx +++ b/e2e/solid-router/basic-file-based/src/routes/params-ps/non-nested/route.tsx @@ -16,7 +16,15 @@ function RouteComponent() { to="./$foo/$bar" params={{ foo: 'foo', bar: 'bar' }} > - /params-ps/non-nested/$foo/$bar + /params-ps/non-nested/foo/bar + + + /params-ps/non-nested/foo2/bar2 diff --git a/e2e/solid-router/basic-file-based/tests/params.spec.ts b/e2e/solid-router/basic-file-based/tests/params.spec.ts index 3a63c934175..e85cd0c6b01 100644 --- a/e2e/solid-router/basic-file-based/tests/params.spec.ts +++ b/e2e/solid-router/basic-file-based/tests/params.spec.ts @@ -145,7 +145,8 @@ test.describe('params operations + non-nested routes', () => { '/params-ps/non-nested/foo/bar', ) await fooBarLink.click() - await page.waitForLoadState('networkidle') + await page.waitForURL('/params-ps/non-nested/foo/bar') + const pagePathname = new URL(page.url()).pathname expect(pagePathname).toBe('/params-ps/non-nested/foo/bar') @@ -158,5 +159,26 @@ test.describe('params operations + non-nested routes', () => { const paramsText = await paramsValue.innerText() const paramsObj = JSON.parse(paramsText) expect(paramsObj).toEqual({ foo: 'foo', bar: 'bar' }) + + const foo2Bar2Link = page.getByTestId('l-to-non-nested-foo2-bar2') + + await expect(foo2Bar2Link).toHaveAttribute( + 'href', + '/params-ps/non-nested/foo2/bar2', + ) + await foo2Bar2Link.click() + await page.waitForURL('/params-ps/non-nested/foo2/bar2') + const pagePathname2 = new URL(page.url()).pathname + expect(pagePathname2).toBe('/params-ps/non-nested/foo2/bar2') + + const foo2ParamsValue = page.getByTestId('foo-params-value') + const foo2ParamsText = await foo2ParamsValue.innerText() + const foo2ParamsObj = JSON.parse(foo2ParamsText) + expect(foo2ParamsObj).toEqual({ foo: 'foo2' }) + + const params2Value = page.getByTestId('foo-bar-params-value') + const params2Text = await params2Value.innerText() + const params2Obj = JSON.parse(params2Text) + expect(params2Obj).toEqual({ foo: 'foo2', bar: 'bar2' }) }) }) diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts index 7402001cdf4..b7d312c6b37 100644 --- a/packages/router-core/src/path.ts +++ b/packages/router-core/src/path.ts @@ -265,8 +265,11 @@ function baseParsePathname(pathname: string): ReadonlyArray { segments.push( ...split.map((part): Segment => { + // strip tailing underscore for non-nested paths + const partToMatch = part.slice(-1) === '_' ? part.slice(0, -1) : part + // Check for wildcard with curly braces: prefix{$}suffix - const wildcardBracesMatch = part.match(WILDCARD_W_CURLY_BRACES_RE) + const wildcardBracesMatch = partToMatch.match(WILDCARD_W_CURLY_BRACES_RE) if (wildcardBracesMatch) { const prefix = wildcardBracesMatch[1] const suffix = wildcardBracesMatch[2] @@ -279,7 +282,7 @@ function baseParsePathname(pathname: string): ReadonlyArray { } // Check for optional parameter format: prefix{-$paramName}suffix - const optionalParamBracesMatch = part.match( + const optionalParamBracesMatch = partToMatch.match( OPTIONAL_PARAM_W_CURLY_BRACES_RE, ) if (optionalParamBracesMatch) { @@ -295,7 +298,7 @@ function baseParsePathname(pathname: string): ReadonlyArray { } // Check for the new parameter format: prefix{$paramName}suffix - const paramBracesMatch = part.match(PARAM_W_CURLY_BRACES_RE) + const paramBracesMatch = partToMatch.match(PARAM_W_CURLY_BRACES_RE) if (paramBracesMatch) { const prefix = paramBracesMatch[1] const paramName = paramBracesMatch[2] @@ -309,8 +312,9 @@ function baseParsePathname(pathname: string): ReadonlyArray { } // Check for bare parameter format: $paramName (without curly braces) - if (PARAM_RE.test(part)) { - const paramName = part.substring(1) + if (PARAM_RE.test(partToMatch)) { + const paramName = partToMatch.substring(1) + return { type: SEGMENT_TYPE_PARAM, value: '$' + paramName, @@ -320,7 +324,7 @@ function baseParsePathname(pathname: string): ReadonlyArray { } // Check for bare wildcard: $ (without curly braces) - if (WILDCARD_RE.test(part)) { + if (WILDCARD_RE.test(partToMatch)) { return { type: SEGMENT_TYPE_WILDCARD, value: '$', @@ -332,8 +336,8 @@ function baseParsePathname(pathname: string): ReadonlyArray { // Handle regular pathname segment return { type: SEGMENT_TYPE_PATHNAME, - value: part.includes('%25') - ? part + value: partToMatch.includes('%25') + ? partToMatch .split('%25') .map((segment) => decodeURI(segment)) .join('%25') diff --git a/packages/router-core/tests/path.test.ts b/packages/router-core/tests/path.test.ts index 2a241bcb77d..d3580cc9a9a 100644 --- a/packages/router-core/tests/path.test.ts +++ b/packages/router-core/tests/path.test.ts @@ -432,6 +432,12 @@ describe('interpolatePath', () => { params: { _splat: 'sean/cassiere' }, result: '/users/sean/cassiere', }, + { + name: 'should interpolate the non-nested path', + path: '/users/$id_', + params: { id: '123' }, + result: '/users/123', + }, ])('$name', ({ path, params, decodeCharMap, result }) => { expect( interpolatePath({ @@ -654,6 +660,14 @@ describe('matchPathname', () => { }, expectedMatchedParams: { id: '123' }, }, + { + name: 'should match and return the non-nested named path params', + input: '/users/123', + matchingOptions: { + to: '/users/$id_', + }, + expectedMatchedParams: { id: '123' }, + }, { name: 'should match and return the the splat param', input: '/users/123', @@ -893,6 +907,15 @@ describe('parsePathname', () => { { type: SEGMENT_TYPE_PARAM, value: '$bar' }, ], }, + { + name: 'should handle non-nested named params', + to: '/foo/$bar_', + expected: [ + { type: SEGMENT_TYPE_PATHNAME, value: '/' }, + { type: SEGMENT_TYPE_PATHNAME, value: 'foo' }, + { type: SEGMENT_TYPE_PARAM, value: '$bar' }, + ], + }, { name: 'should handle named params at the root', to: '/$bar',