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',