diff --git a/e2e/react-router/basic-file-based/src/routeTree.gen.ts b/e2e/react-router/basic-file-based/src/routeTree.gen.ts
index 2ba33a64143..2daaf9a821e 100644
--- a/e2e/react-router/basic-file-based/src/routeTree.gen.ts
+++ b/e2e/react-router/basic-file-based/src/routeTree.gen.ts
@@ -21,6 +21,7 @@ import { Route as ComponentTypesTestRouteImport } from './routes/component-types
import { Route as AnchorRouteImport } from './routes/anchor'
import { Route as LayoutRouteImport } from './routes/_layout'
import { Route as SearchParamsRouteRouteImport } from './routes/search-params/route'
+import { Route as NonNestedRouteRouteImport } from './routes/non-nested/route'
import { Route as IndexRouteImport } from './routes/index'
import { Route as SearchParamsIndexRouteImport } from './routes/search-params/index'
import { Route as RelativeIndexRouteImport } from './routes/relative/index'
@@ -31,6 +32,7 @@ import { Route as StructuralSharingEnabledRouteImport } from './routes/structura
import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default'
import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
+import { Route as NonNestedBazRouteImport } from './routes/non-nested/baz'
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
import { Route as groupLazyinsideRouteImport } from './routes/(group)/lazyinside'
import { Route as groupInsideRouteImport } from './routes/(group)/inside'
@@ -58,6 +60,7 @@ import { Route as ParamsPsWildcardPrefixChar123Char125RouteImport } from './rout
import { Route as ParamsPsWildcardSplatRouteImport } from './routes/params-ps/wildcard/$'
import { Route as ParamsPsNamedChar123fooChar125suffixRouteImport } from './routes/params-ps/named/{$foo}suffix'
import { Route as ParamsPsNamedPrefixChar123fooChar125RouteImport } from './routes/params-ps/named/prefix{$foo}'
+import { Route as NonNestedBazBazidRouteImport } from './routes/non-nested/baz.$bazid'
import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
import { Route as groupSubfolderInsideRouteImport } from './routes/(group)/subfolder/inside'
@@ -71,6 +74,7 @@ import { Route as RelativeLinkWithSearchIndexRouteImport } from './routes/relati
import { Route as RelativeLinkPathIndexRouteImport } from './routes/relative/link/path/index'
import { Route as RelativeLinkNestedIndexRouteImport } from './routes/relative/link/nested/index'
import { Route as ParamsPsNonNestedFooBarRouteImport } from './routes/params-ps/non-nested/$foo_/$bar'
+import { Route as NonNestedBazBazidEditRouteImport } from './routes/non-nested/baz_.$bazid.edit'
import { Route as ParamsPsNamedFooBarRouteRouteImport } from './routes/params-ps/named/$foo/$bar.route'
import { Route as RelativeUseNavigatePathPathIndexRouteImport } from './routes/relative/useNavigate/path/$path/index'
import { Route as RelativeUseNavigateNestedDeepIndexRouteImport } from './routes/relative/useNavigate/nested/deep/index'
@@ -134,6 +138,11 @@ const SearchParamsRouteRoute = SearchParamsRouteRouteImport.update({
path: '/search-params',
getParentRoute: () => rootRouteImport,
} as any)
+const NonNestedRouteRoute = NonNestedRouteRouteImport.update({
+ id: '/non-nested',
+ path: '/non-nested',
+ getParentRoute: () => rootRouteImport,
+} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
@@ -185,6 +194,11 @@ const PostsPostIdRoute = PostsPostIdRouteImport.update({
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
+const NonNestedBazRoute = NonNestedBazRouteImport.update({
+ id: '/baz',
+ path: '/baz',
+ getParentRoute: () => NonNestedRouteRoute,
+} as any)
const LayoutLayout2Route = LayoutLayout2RouteImport.update({
id: '/_layout-2',
getParentRoute: () => LayoutRoute,
@@ -331,6 +345,11 @@ const ParamsPsNamedPrefixChar123fooChar125Route =
path: '/params-ps/named/prefix{$foo}',
getParentRoute: () => rootRouteImport,
} as any)
+const NonNestedBazBazidRoute = NonNestedBazBazidRouteImport.update({
+ id: '/$bazid',
+ path: '/$bazid',
+ getParentRoute: () => NonNestedBazRoute,
+} as any)
const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({
id: '/layout-b',
path: '/layout-b',
@@ -401,6 +420,11 @@ const ParamsPsNonNestedFooBarRoute = ParamsPsNonNestedFooBarRouteImport.update({
path: '/$bar',
getParentRoute: () => ParamsPsNonNestedFooRouteRoute,
} as any)
+const NonNestedBazBazidEditRoute = NonNestedBazBazidEditRouteImport.update({
+ id: '/baz_/$bazid/edit',
+ path: '/baz/$bazid/edit',
+ getParentRoute: () => NonNestedRouteRoute,
+} as any)
const ParamsPsNamedFooBarRouteRoute =
ParamsPsNamedFooBarRouteRouteImport.update({
id: '/$bar',
@@ -439,6 +463,7 @@ const ParamsPsNamedFooBarBazRoute = ParamsPsNamedFooBarBazRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof groupLayoutRouteWithChildren
+ '/non-nested': typeof NonNestedRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
'/anchor': typeof AnchorRoute
'/component-types-test': typeof ComponentTypesTestRoute
@@ -454,6 +479,7 @@ export interface FileRoutesByFullPath {
'/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
'/inside': typeof groupInsideRoute
'/lazyinside': typeof groupLazyinsideRoute
+ '/non-nested/baz': typeof NonNestedBazRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
'/search-params/default': typeof SearchParamsDefaultRoute
@@ -469,6 +495,7 @@ export interface FileRoutesByFullPath {
'/subfolder/inside': typeof groupSubfolderInsideRoute
'/layout-a': typeof LayoutLayout2LayoutARoute
'/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/non-nested/baz/$bazid': typeof NonNestedBazBazidRoute
'/params-ps/named/prefix{$foo}': typeof ParamsPsNamedPrefixChar123fooChar125Route
'/params-ps/named/{$foo}suffix': typeof ParamsPsNamedChar123fooChar125suffixRoute
'/params-ps/wildcard/$': typeof ParamsPsWildcardSplatRoute
@@ -489,6 +516,7 @@ export interface FileRoutesByFullPath {
'/params-ps/wildcard': typeof ParamsPsWildcardIndexRoute
'/redirect/$target/': typeof RedirectTargetIndexRoute
'/params-ps/named/$foo/$bar': typeof ParamsPsNamedFooBarRouteRouteWithChildren
+ '/non-nested/baz/$bazid/edit': typeof NonNestedBazBazidEditRoute
'/params-ps/non-nested/$foo/$bar': typeof ParamsPsNonNestedFooBarRoute
'/relative/link/nested': typeof RelativeLinkNestedIndexRoute
'/relative/link/path': typeof RelativeLinkPathIndexRoute
@@ -504,6 +532,7 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/': typeof groupLayoutRouteWithChildren
+ '/non-nested': typeof NonNestedRouteRouteWithChildren
'/anchor': typeof AnchorRoute
'/component-types-test': typeof ComponentTypesTestRoute
'/editing-a': typeof EditingARoute
@@ -517,6 +546,7 @@ export interface FileRoutesByTo {
'/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
'/inside': typeof groupInsideRoute
'/lazyinside': typeof groupLazyinsideRoute
+ '/non-nested/baz': typeof NonNestedBazRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
'/search-params/default': typeof SearchParamsDefaultRoute
'/structural-sharing/$enabled': typeof StructuralSharingEnabledRoute
@@ -531,6 +561,7 @@ export interface FileRoutesByTo {
'/subfolder/inside': typeof groupSubfolderInsideRoute
'/layout-a': typeof LayoutLayout2LayoutARoute
'/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/non-nested/baz/$bazid': typeof NonNestedBazBazidRoute
'/params-ps/named/prefix{$foo}': typeof ParamsPsNamedPrefixChar123fooChar125Route
'/params-ps/named/{$foo}suffix': typeof ParamsPsNamedChar123fooChar125suffixRoute
'/params-ps/wildcard/$': typeof ParamsPsWildcardSplatRoute
@@ -551,6 +582,7 @@ export interface FileRoutesByTo {
'/params-ps/wildcard': typeof ParamsPsWildcardIndexRoute
'/redirect/$target': typeof RedirectTargetIndexRoute
'/params-ps/named/$foo/$bar': typeof ParamsPsNamedFooBarRouteRouteWithChildren
+ '/non-nested/baz/$bazid/edit': typeof NonNestedBazBazidEditRoute
'/params-ps/non-nested/$foo/$bar': typeof ParamsPsNonNestedFooBarRoute
'/relative/link/nested': typeof RelativeLinkNestedIndexRoute
'/relative/link/path': typeof RelativeLinkPathIndexRoute
@@ -567,6 +599,7 @@ export interface FileRoutesByTo {
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
+ '/non-nested': typeof NonNestedRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
'/_layout': typeof LayoutRouteWithChildren
'/anchor': typeof AnchorRoute
@@ -586,6 +619,7 @@ export interface FileRoutesById {
'/(group)/inside': typeof groupInsideRoute
'/(group)/lazyinside': typeof groupLazyinsideRoute
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
+ '/non-nested/baz': typeof NonNestedBazRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
'/search-params/default': typeof SearchParamsDefaultRoute
@@ -601,6 +635,7 @@ export interface FileRoutesById {
'/(group)/subfolder/inside': typeof groupSubfolderInsideRoute
'/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
'/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/non-nested/baz/$bazid': typeof NonNestedBazBazidRoute
'/params-ps/named/prefix{$foo}': typeof ParamsPsNamedPrefixChar123fooChar125Route
'/params-ps/named/{$foo}suffix': typeof ParamsPsNamedChar123fooChar125suffixRoute
'/params-ps/wildcard/$': typeof ParamsPsWildcardSplatRoute
@@ -621,6 +656,7 @@ export interface FileRoutesById {
'/params-ps/wildcard/': typeof ParamsPsWildcardIndexRoute
'/redirect/$target/': typeof RedirectTargetIndexRoute
'/params-ps/named/$foo/$bar': typeof ParamsPsNamedFooBarRouteRouteWithChildren
+ '/non-nested/baz_/$bazid/edit': typeof NonNestedBazBazidEditRoute
'/params-ps/non-nested/$foo_/$bar': typeof ParamsPsNonNestedFooBarRoute
'/relative/link/nested/': typeof RelativeLinkNestedIndexRoute
'/relative/link/path/': typeof RelativeLinkPathIndexRoute
@@ -638,6 +674,7 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
+ | '/non-nested'
| '/search-params'
| '/anchor'
| '/component-types-test'
@@ -653,6 +690,7 @@ export interface FileRouteTypes {
| '/onlyrouteinside'
| '/inside'
| '/lazyinside'
+ | '/non-nested/baz'
| '/posts/$postId'
| '/redirect/$target'
| '/search-params/default'
@@ -668,6 +706,7 @@ export interface FileRouteTypes {
| '/subfolder/inside'
| '/layout-a'
| '/layout-b'
+ | '/non-nested/baz/$bazid'
| '/params-ps/named/prefix{$foo}'
| '/params-ps/named/{$foo}suffix'
| '/params-ps/wildcard/$'
@@ -688,6 +727,7 @@ export interface FileRouteTypes {
| '/params-ps/wildcard'
| '/redirect/$target/'
| '/params-ps/named/$foo/$bar'
+ | '/non-nested/baz/$bazid/edit'
| '/params-ps/non-nested/$foo/$bar'
| '/relative/link/nested'
| '/relative/link/path'
@@ -703,6 +743,7 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
+ | '/non-nested'
| '/anchor'
| '/component-types-test'
| '/editing-a'
@@ -716,6 +757,7 @@ export interface FileRouteTypes {
| '/onlyrouteinside'
| '/inside'
| '/lazyinside'
+ | '/non-nested/baz'
| '/posts/$postId'
| '/search-params/default'
| '/structural-sharing/$enabled'
@@ -730,6 +772,7 @@ export interface FileRouteTypes {
| '/subfolder/inside'
| '/layout-a'
| '/layout-b'
+ | '/non-nested/baz/$bazid'
| '/params-ps/named/prefix{$foo}'
| '/params-ps/named/{$foo}suffix'
| '/params-ps/wildcard/$'
@@ -750,6 +793,7 @@ export interface FileRouteTypes {
| '/params-ps/wildcard'
| '/redirect/$target'
| '/params-ps/named/$foo/$bar'
+ | '/non-nested/baz/$bazid/edit'
| '/params-ps/non-nested/$foo/$bar'
| '/relative/link/nested'
| '/relative/link/path'
@@ -765,6 +809,7 @@ export interface FileRouteTypes {
id:
| '__root__'
| '/'
+ | '/non-nested'
| '/search-params'
| '/_layout'
| '/anchor'
@@ -784,6 +829,7 @@ export interface FileRouteTypes {
| '/(group)/inside'
| '/(group)/lazyinside'
| '/_layout/_layout-2'
+ | '/non-nested/baz'
| '/posts/$postId'
| '/redirect/$target'
| '/search-params/default'
@@ -799,6 +845,7 @@ export interface FileRouteTypes {
| '/(group)/subfolder/inside'
| '/_layout/_layout-2/layout-a'
| '/_layout/_layout-2/layout-b'
+ | '/non-nested/baz/$bazid'
| '/params-ps/named/prefix{$foo}'
| '/params-ps/named/{$foo}suffix'
| '/params-ps/wildcard/$'
@@ -819,6 +866,7 @@ export interface FileRouteTypes {
| '/params-ps/wildcard/'
| '/redirect/$target/'
| '/params-ps/named/$foo/$bar'
+ | '/non-nested/baz_/$bazid/edit'
| '/params-ps/non-nested/$foo_/$bar'
| '/relative/link/nested/'
| '/relative/link/path/'
@@ -835,6 +883,7 @@ export interface FileRouteTypes {
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
+ NonNestedRouteRoute: typeof NonNestedRouteRouteWithChildren
SearchParamsRouteRoute: typeof SearchParamsRouteRouteWithChildren
LayoutRoute: typeof LayoutRouteWithChildren
AnchorRoute: typeof AnchorRoute
@@ -949,6 +998,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SearchParamsRouteRouteImport
parentRoute: typeof rootRouteImport
}
+ '/non-nested': {
+ id: '/non-nested'
+ path: '/non-nested'
+ fullPath: '/non-nested'
+ preLoaderRoute: typeof NonNestedRouteRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/': {
id: '/'
path: '/'
@@ -1019,6 +1075,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
+ '/non-nested/baz': {
+ id: '/non-nested/baz'
+ path: '/baz'
+ fullPath: '/non-nested/baz'
+ preLoaderRoute: typeof NonNestedBazRouteImport
+ parentRoute: typeof NonNestedRouteRoute
+ }
'/_layout/_layout-2': {
id: '/_layout/_layout-2'
path: ''
@@ -1208,6 +1271,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ParamsPsNamedPrefixChar123fooChar125RouteImport
parentRoute: typeof rootRouteImport
}
+ '/non-nested/baz/$bazid': {
+ id: '/non-nested/baz/$bazid'
+ path: '/$bazid'
+ fullPath: '/non-nested/baz/$bazid'
+ preLoaderRoute: typeof NonNestedBazBazidRouteImport
+ parentRoute: typeof NonNestedBazRoute
+ }
'/_layout/_layout-2/layout-b': {
id: '/_layout/_layout-2/layout-b'
path: '/layout-b'
@@ -1299,6 +1369,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ParamsPsNonNestedFooBarRouteImport
parentRoute: typeof ParamsPsNonNestedFooRouteRoute
}
+ '/non-nested/baz_/$bazid/edit': {
+ id: '/non-nested/baz_/$bazid/edit'
+ path: '/baz/$bazid/edit'
+ fullPath: '/non-nested/baz/$bazid/edit'
+ preLoaderRoute: typeof NonNestedBazBazidEditRouteImport
+ parentRoute: typeof NonNestedRouteRoute
+ }
'/params-ps/named/$foo/$bar': {
id: '/params-ps/named/$foo/$bar'
path: '/$bar'
@@ -1344,6 +1421,32 @@ declare module '@tanstack/react-router' {
}
}
+interface NonNestedBazRouteChildren {
+ NonNestedBazBazidRoute: typeof NonNestedBazBazidRoute
+}
+
+const NonNestedBazRouteChildren: NonNestedBazRouteChildren = {
+ NonNestedBazBazidRoute: NonNestedBazBazidRoute,
+}
+
+const NonNestedBazRouteWithChildren = NonNestedBazRoute._addFileChildren(
+ NonNestedBazRouteChildren,
+)
+
+interface NonNestedRouteRouteChildren {
+ NonNestedBazRoute: typeof NonNestedBazRouteWithChildren
+ NonNestedBazBazidEditRoute: typeof NonNestedBazBazidEditRoute
+}
+
+const NonNestedRouteRouteChildren: NonNestedRouteRouteChildren = {
+ NonNestedBazRoute: NonNestedBazRouteWithChildren,
+ NonNestedBazBazidEditRoute: NonNestedBazBazidEditRoute,
+}
+
+const NonNestedRouteRouteWithChildren = NonNestedRouteRoute._addFileChildren(
+ NonNestedRouteRouteChildren,
+)
+
interface SearchParamsRouteRouteChildren {
SearchParamsDefaultRoute: typeof SearchParamsDefaultRoute
SearchParamsIndexRoute: typeof SearchParamsIndexRoute
@@ -1549,6 +1652,7 @@ const ParamsPsNamedFooRouteRouteWithChildren =
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
+ NonNestedRouteRoute: NonNestedRouteRouteWithChildren,
SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren,
LayoutRoute: LayoutRouteWithChildren,
AnchorRoute: AnchorRoute,
diff --git a/e2e/react-router/basic-file-based/src/routes/non-nested/baz.$bazid.tsx b/e2e/react-router/basic-file-based/src/routes/non-nested/baz.$bazid.tsx
new file mode 100644
index 00000000000..7160b5d7aa6
--- /dev/null
+++ b/e2e/react-router/basic-file-based/src/routes/non-nested/baz.$bazid.tsx
@@ -0,0 +1,23 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/non-nested/baz/$bazid')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const params = Route.useParams()
+
+ return (
+ <>
+
+ Hello "/non-nested/baz/$bazid"!
+
+
+ params:{' '}
+
+ {JSON.stringify(params)}
+
+
+ >
+ )
+}
diff --git a/e2e/react-router/basic-file-based/src/routes/non-nested/baz.tsx b/e2e/react-router/basic-file-based/src/routes/non-nested/baz.tsx
new file mode 100644
index 00000000000..149903da04c
--- /dev/null
+++ b/e2e/react-router/basic-file-based/src/routes/non-nested/baz.tsx
@@ -0,0 +1,14 @@
+import { Outlet, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/non-nested/baz')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+ <>
+ Hello "/non-nested/baz"!
+
+ >
+ )
+}
diff --git a/e2e/react-router/basic-file-based/src/routes/non-nested/baz_.$bazid.edit.tsx b/e2e/react-router/basic-file-based/src/routes/non-nested/baz_.$bazid.edit.tsx
new file mode 100644
index 00000000000..491385c9901
--- /dev/null
+++ b/e2e/react-router/basic-file-based/src/routes/non-nested/baz_.$bazid.edit.tsx
@@ -0,0 +1,22 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/non-nested/baz_/$bazid/edit')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const params = Route.useParams()
+ return (
+ <>
+
+ Hello "/non-nested/baz_/$bazid/edit"!
+
+
+ params:{' '}
+
+ {JSON.stringify(params)}
+
+
+ >
+ )
+}
diff --git a/e2e/react-router/basic-file-based/src/routes/non-nested/route.tsx b/e2e/react-router/basic-file-based/src/routes/non-nested/route.tsx
new file mode 100644
index 00000000000..83a42874acb
--- /dev/null
+++ b/e2e/react-router/basic-file-based/src/routes/non-nested/route.tsx
@@ -0,0 +1,38 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/non-nested')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
+ Non-nested paths
+
+
+ -
+
+ /non-nested/baz/123
+
+
+ -
+
+ /non-nested/baz/456/edit
+
+
+
+
+
+ )
+}
diff --git a/e2e/react-router/basic-file-based/tests/app.spec.ts b/e2e/react-router/basic-file-based/tests/app.spec.ts
index d825ca7b51f..38de31e8299 100644
--- a/e2e/react-router/basic-file-based/tests/app.spec.ts
+++ b/e2e/react-router/basic-file-based/tests/app.spec.ts
@@ -308,3 +308,38 @@ test('Should remount deps when remountDeps does change ', async ({ page }) => {
'Page component mounts: 2',
)
})
+
+test('Should not nest non-nested paths', async ({ page }) => {
+ await page.goto('/non-nested')
+ await page.waitForURL('/non-nested')
+ const nonNestedPathHeading = page.getByTestId('non-nested-path-heading')
+ const bazIdLink = page.getByTestId('l-to-non-nested-bazid')
+ const bazIdEditLink = page.getByTestId('l-to-non-nested-bazid-edit')
+
+ await expect(nonNestedPathHeading).toBeInViewport()
+ await expect(bazIdLink).toHaveAttribute('href', '/non-nested/baz/123')
+ await expect(bazIdEditLink).toHaveAttribute(
+ 'href',
+ '/non-nested/baz/456/edit',
+ )
+
+ await bazIdLink.click()
+ await page.waitForURL('/non-nested/baz/123')
+ const bazHeading = page.getByTestId('non-nested-baz-heading')
+ const bazIdHeading = page.getByTestId('non-nested-bazid-heading')
+ const bazIdParam = page.getByTestId('non-nested-bazid-param')
+ await expect(nonNestedPathHeading).toBeInViewport()
+ await expect(bazHeading).toBeInViewport()
+ await expect(bazIdHeading).toBeInViewport()
+ await expect(bazIdParam).toContainText(JSON.stringify({ bazid: '123' }))
+
+ await bazIdEditLink.click()
+ await page.waitForURL('/non-nested/baz/456/edit')
+ const bazIdEditHeading = page.getByTestId('non-nested-bazid-edit-heading')
+ const bazIdEditParam = page.getByTestId('non-nested-bazid-edit-param')
+ await expect(nonNestedPathHeading).toBeInViewport()
+ await expect(bazHeading).toBeHidden()
+ await expect(bazIdHeading).toBeHidden()
+ await expect(bazIdEditHeading).toBeInViewport()
+ await expect(bazIdEditParam).toContainText(JSON.stringify({ bazid: '456' }))
+})
diff --git a/e2e/solid-router/basic-file-based/src/routeTree.gen.ts b/e2e/solid-router/basic-file-based/src/routeTree.gen.ts
index f4b02f10a84..23092968648 100644
--- a/e2e/solid-router/basic-file-based/src/routeTree.gen.ts
+++ b/e2e/solid-router/basic-file-based/src/routeTree.gen.ts
@@ -20,6 +20,7 @@ import { Route as ComponentTypesTestRouteImport } from './routes/component-types
import { Route as AnchorRouteImport } from './routes/anchor'
import { Route as LayoutRouteImport } from './routes/_layout'
import { Route as SearchParamsRouteRouteImport } from './routes/search-params/route'
+import { Route as NonNestedRouteRouteImport } from './routes/non-nested/route'
import { Route as IndexRouteImport } from './routes/index'
import { Route as SearchParamsIndexRouteImport } from './routes/search-params/index'
import { Route as RelativeIndexRouteImport } from './routes/relative/index'
@@ -29,6 +30,7 @@ import { Route as ParamsPsIndexRouteImport } from './routes/params-ps/index'
import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default'
import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
+import { Route as NonNestedBazRouteImport } from './routes/non-nested/baz'
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
import { Route as groupLazyinsideRouteImport } from './routes/(group)/lazyinside'
import { Route as groupInsideRouteImport } from './routes/(group)/inside'
@@ -49,6 +51,7 @@ import { Route as RedirectTargetViaLoaderRouteImport } from './routes/redirect/$
import { Route as RedirectTargetViaBeforeLoadRouteImport } from './routes/redirect/$target/via-beforeLoad'
import { Route as PostsPostIdEditRouteImport } from './routes/posts_.$postId.edit'
import { Route as ParamsSingleValueRouteImport } from './routes/params.single.$value'
+import { Route as NonNestedBazBazidRouteImport } from './routes/non-nested/baz.$bazid'
import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
import { Route as groupSubfolderInsideRouteImport } from './routes/(group)/subfolder/inside'
@@ -62,6 +65,7 @@ import { Route as RelativeLinkWithSearchIndexRouteImport } from './routes/relati
import { Route as RelativeLinkPathIndexRouteImport } from './routes/relative/link/path/index'
import { Route as RelativeLinkNestedIndexRouteImport } from './routes/relative/link/nested/index'
import { Route as ParamsPsNonNestedFooBarRouteImport } from './routes/params-ps/non-nested/$foo_/$bar'
+import { Route as NonNestedBazBazidEditRouteImport } from './routes/non-nested/baz_.$bazid.edit'
import { Route as ParamsPsNamedFooBarRouteRouteImport } from './routes/params-ps/named/$foo/$bar.route'
import { Route as RelativeUseNavigatePathPathIndexRouteImport } from './routes/relative/useNavigate/path/$path/index'
import { Route as RelativeUseNavigateNestedDeepIndexRouteImport } from './routes/relative/useNavigate/nested/deep/index'
@@ -119,6 +123,11 @@ const SearchParamsRouteRoute = SearchParamsRouteRouteImport.update({
path: '/search-params',
getParentRoute: () => rootRouteImport,
} as any)
+const NonNestedRouteRoute = NonNestedRouteRouteImport.update({
+ id: '/non-nested',
+ path: '/non-nested',
+ getParentRoute: () => rootRouteImport,
+} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
@@ -164,6 +173,11 @@ const PostsPostIdRoute = PostsPostIdRouteImport.update({
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
+const NonNestedBazRoute = NonNestedBazRouteImport.update({
+ id: '/baz',
+ path: '/baz',
+ getParentRoute: () => NonNestedRouteRoute,
+} as any)
const LayoutLayout2Route = LayoutLayout2RouteImport.update({
id: '/_layout-2',
getParentRoute: () => LayoutRoute,
@@ -271,6 +285,11 @@ const ParamsSingleValueRoute = ParamsSingleValueRouteImport.update({
path: '/params/single/$value',
getParentRoute: () => rootRouteImport,
} as any)
+const NonNestedBazBazidRoute = NonNestedBazBazidRouteImport.update({
+ id: '/$bazid',
+ path: '/$bazid',
+ getParentRoute: () => NonNestedBazRoute,
+} as any)
const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({
id: '/layout-b',
path: '/layout-b',
@@ -341,6 +360,11 @@ const ParamsPsNonNestedFooBarRoute = ParamsPsNonNestedFooBarRouteImport.update({
path: '/$bar',
getParentRoute: () => ParamsPsNonNestedFooRouteRoute,
} as any)
+const NonNestedBazBazidEditRoute = NonNestedBazBazidEditRouteImport.update({
+ id: '/baz_/$bazid/edit',
+ path: '/baz/$bazid/edit',
+ getParentRoute: () => NonNestedRouteRoute,
+} as any)
const ParamsPsNamedFooBarRouteRoute =
ParamsPsNamedFooBarRouteRouteImport.update({
id: '/$bar',
@@ -379,6 +403,7 @@ const ParamsPsNamedFooBarBazRoute = ParamsPsNamedFooBarBazRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof groupLayoutRouteWithChildren
+ '/non-nested': typeof NonNestedRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
'/anchor': typeof AnchorRoute
'/component-types-test': typeof ComponentTypesTestRoute
@@ -393,6 +418,7 @@ export interface FileRoutesByFullPath {
'/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
'/inside': typeof groupInsideRoute
'/lazyinside': typeof groupLazyinsideRoute
+ '/non-nested/baz': typeof NonNestedBazRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
'/search-params/default': typeof SearchParamsDefaultRoute
@@ -407,6 +433,7 @@ export interface FileRoutesByFullPath {
'/subfolder/inside': typeof groupSubfolderInsideRoute
'/layout-a': typeof LayoutLayout2LayoutARoute
'/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/non-nested/baz/$bazid': typeof NonNestedBazBazidRoute
'/params/single/$value': typeof ParamsSingleValueRoute
'/posts/$postId/edit': typeof PostsPostIdEditRoute
'/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
@@ -420,6 +447,7 @@ export interface FileRoutesByFullPath {
'/relative/useNavigate/relative-useNavigate-b': typeof RelativeUseNavigateRelativeUseNavigateBRoute
'/redirect/$target/': typeof RedirectTargetIndexRoute
'/params-ps/named/$foo/$bar': typeof ParamsPsNamedFooBarRouteRouteWithChildren
+ '/non-nested/baz/$bazid/edit': typeof NonNestedBazBazidEditRoute
'/params-ps/non-nested/$foo/$bar': typeof ParamsPsNonNestedFooBarRoute
'/relative/link/nested': typeof RelativeLinkNestedIndexRoute
'/relative/link/path': typeof RelativeLinkPathIndexRoute
@@ -435,6 +463,7 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/': typeof groupLayoutRouteWithChildren
+ '/non-nested': typeof NonNestedRouteRouteWithChildren
'/anchor': typeof AnchorRoute
'/component-types-test': typeof ComponentTypesTestRoute
'/editing-a': typeof EditingARoute
@@ -447,6 +476,7 @@ export interface FileRoutesByTo {
'/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
'/inside': typeof groupInsideRoute
'/lazyinside': typeof groupLazyinsideRoute
+ '/non-nested/baz': typeof NonNestedBazRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
'/search-params/default': typeof SearchParamsDefaultRoute
'/params-ps': typeof ParamsPsIndexRoute
@@ -460,6 +490,7 @@ export interface FileRoutesByTo {
'/subfolder/inside': typeof groupSubfolderInsideRoute
'/layout-a': typeof LayoutLayout2LayoutARoute
'/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/non-nested/baz/$bazid': typeof NonNestedBazBazidRoute
'/params/single/$value': typeof ParamsSingleValueRoute
'/posts/$postId/edit': typeof PostsPostIdEditRoute
'/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
@@ -473,6 +504,7 @@ export interface FileRoutesByTo {
'/relative/useNavigate/relative-useNavigate-b': typeof RelativeUseNavigateRelativeUseNavigateBRoute
'/redirect/$target': typeof RedirectTargetIndexRoute
'/params-ps/named/$foo/$bar': typeof ParamsPsNamedFooBarRouteRouteWithChildren
+ '/non-nested/baz/$bazid/edit': typeof NonNestedBazBazidEditRoute
'/params-ps/non-nested/$foo/$bar': typeof ParamsPsNonNestedFooBarRoute
'/relative/link/nested': typeof RelativeLinkNestedIndexRoute
'/relative/link/path': typeof RelativeLinkPathIndexRoute
@@ -489,6 +521,7 @@ export interface FileRoutesByTo {
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
+ '/non-nested': typeof NonNestedRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
'/_layout': typeof LayoutRouteWithChildren
'/anchor': typeof AnchorRoute
@@ -507,6 +540,7 @@ export interface FileRoutesById {
'/(group)/inside': typeof groupInsideRoute
'/(group)/lazyinside': typeof groupLazyinsideRoute
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
+ '/non-nested/baz': typeof NonNestedBazRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
'/search-params/default': typeof SearchParamsDefaultRoute
@@ -521,6 +555,7 @@ export interface FileRoutesById {
'/(group)/subfolder/inside': typeof groupSubfolderInsideRoute
'/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
'/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/non-nested/baz/$bazid': typeof NonNestedBazBazidRoute
'/params/single/$value': typeof ParamsSingleValueRoute
'/posts_/$postId/edit': typeof PostsPostIdEditRoute
'/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
@@ -534,6 +569,7 @@ export interface FileRoutesById {
'/relative/useNavigate/relative-useNavigate-b': typeof RelativeUseNavigateRelativeUseNavigateBRoute
'/redirect/$target/': typeof RedirectTargetIndexRoute
'/params-ps/named/$foo/$bar': typeof ParamsPsNamedFooBarRouteRouteWithChildren
+ '/non-nested/baz_/$bazid/edit': typeof NonNestedBazBazidEditRoute
'/params-ps/non-nested/$foo_/$bar': typeof ParamsPsNonNestedFooBarRoute
'/relative/link/nested/': typeof RelativeLinkNestedIndexRoute
'/relative/link/path/': typeof RelativeLinkPathIndexRoute
@@ -551,6 +587,7 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
+ | '/non-nested'
| '/search-params'
| '/anchor'
| '/component-types-test'
@@ -565,6 +602,7 @@ export interface FileRouteTypes {
| '/onlyrouteinside'
| '/inside'
| '/lazyinside'
+ | '/non-nested/baz'
| '/posts/$postId'
| '/redirect/$target'
| '/search-params/default'
@@ -579,6 +617,7 @@ export interface FileRouteTypes {
| '/subfolder/inside'
| '/layout-a'
| '/layout-b'
+ | '/non-nested/baz/$bazid'
| '/params/single/$value'
| '/posts/$postId/edit'
| '/redirect/$target/via-beforeLoad'
@@ -592,6 +631,7 @@ export interface FileRouteTypes {
| '/relative/useNavigate/relative-useNavigate-b'
| '/redirect/$target/'
| '/params-ps/named/$foo/$bar'
+ | '/non-nested/baz/$bazid/edit'
| '/params-ps/non-nested/$foo/$bar'
| '/relative/link/nested'
| '/relative/link/path'
@@ -607,6 +647,7 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
+ | '/non-nested'
| '/anchor'
| '/component-types-test'
| '/editing-a'
@@ -619,6 +660,7 @@ export interface FileRouteTypes {
| '/onlyrouteinside'
| '/inside'
| '/lazyinside'
+ | '/non-nested/baz'
| '/posts/$postId'
| '/search-params/default'
| '/params-ps'
@@ -632,6 +674,7 @@ export interface FileRouteTypes {
| '/subfolder/inside'
| '/layout-a'
| '/layout-b'
+ | '/non-nested/baz/$bazid'
| '/params/single/$value'
| '/posts/$postId/edit'
| '/redirect/$target/via-beforeLoad'
@@ -645,6 +688,7 @@ export interface FileRouteTypes {
| '/relative/useNavigate/relative-useNavigate-b'
| '/redirect/$target'
| '/params-ps/named/$foo/$bar'
+ | '/non-nested/baz/$bazid/edit'
| '/params-ps/non-nested/$foo/$bar'
| '/relative/link/nested'
| '/relative/link/path'
@@ -660,6 +704,7 @@ export interface FileRouteTypes {
id:
| '__root__'
| '/'
+ | '/non-nested'
| '/search-params'
| '/_layout'
| '/anchor'
@@ -678,6 +723,7 @@ export interface FileRouteTypes {
| '/(group)/inside'
| '/(group)/lazyinside'
| '/_layout/_layout-2'
+ | '/non-nested/baz'
| '/posts/$postId'
| '/redirect/$target'
| '/search-params/default'
@@ -692,6 +738,7 @@ export interface FileRouteTypes {
| '/(group)/subfolder/inside'
| '/_layout/_layout-2/layout-a'
| '/_layout/_layout-2/layout-b'
+ | '/non-nested/baz/$bazid'
| '/params/single/$value'
| '/posts_/$postId/edit'
| '/redirect/$target/via-beforeLoad'
@@ -705,6 +752,7 @@ export interface FileRouteTypes {
| '/relative/useNavigate/relative-useNavigate-b'
| '/redirect/$target/'
| '/params-ps/named/$foo/$bar'
+ | '/non-nested/baz_/$bazid/edit'
| '/params-ps/non-nested/$foo_/$bar'
| '/relative/link/nested/'
| '/relative/link/path/'
@@ -721,6 +769,7 @@ export interface FileRouteTypes {
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
+ NonNestedRouteRoute: typeof NonNestedRouteRouteWithChildren
SearchParamsRouteRoute: typeof SearchParamsRouteRouteWithChildren
LayoutRoute: typeof LayoutRouteWithChildren
AnchorRoute: typeof AnchorRoute
@@ -819,6 +868,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof SearchParamsRouteRouteImport
parentRoute: typeof rootRouteImport
}
+ '/non-nested': {
+ id: '/non-nested'
+ path: '/non-nested'
+ fullPath: '/non-nested'
+ preLoaderRoute: typeof NonNestedRouteRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/': {
id: '/'
path: '/'
@@ -882,6 +938,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
+ '/non-nested/baz': {
+ id: '/non-nested/baz'
+ path: '/baz'
+ fullPath: '/non-nested/baz'
+ preLoaderRoute: typeof NonNestedBazRouteImport
+ parentRoute: typeof NonNestedRouteRoute
+ }
'/_layout/_layout-2': {
id: '/_layout/_layout-2'
path: ''
@@ -1022,6 +1085,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof ParamsSingleValueRouteImport
parentRoute: typeof rootRouteImport
}
+ '/non-nested/baz/$bazid': {
+ id: '/non-nested/baz/$bazid'
+ path: '/$bazid'
+ fullPath: '/non-nested/baz/$bazid'
+ preLoaderRoute: typeof NonNestedBazBazidRouteImport
+ parentRoute: typeof NonNestedBazRoute
+ }
'/_layout/_layout-2/layout-b': {
id: '/_layout/_layout-2/layout-b'
path: '/layout-b'
@@ -1113,6 +1183,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof ParamsPsNonNestedFooBarRouteImport
parentRoute: typeof ParamsPsNonNestedFooRouteRoute
}
+ '/non-nested/baz_/$bazid/edit': {
+ id: '/non-nested/baz_/$bazid/edit'
+ path: '/baz/$bazid/edit'
+ fullPath: '/non-nested/baz/$bazid/edit'
+ preLoaderRoute: typeof NonNestedBazBazidEditRouteImport
+ parentRoute: typeof NonNestedRouteRoute
+ }
'/params-ps/named/$foo/$bar': {
id: '/params-ps/named/$foo/$bar'
path: '/$bar'
@@ -1158,6 +1235,32 @@ declare module '@tanstack/solid-router' {
}
}
+interface NonNestedBazRouteChildren {
+ NonNestedBazBazidRoute: typeof NonNestedBazBazidRoute
+}
+
+const NonNestedBazRouteChildren: NonNestedBazRouteChildren = {
+ NonNestedBazBazidRoute: NonNestedBazBazidRoute,
+}
+
+const NonNestedBazRouteWithChildren = NonNestedBazRoute._addFileChildren(
+ NonNestedBazRouteChildren,
+)
+
+interface NonNestedRouteRouteChildren {
+ NonNestedBazRoute: typeof NonNestedBazRouteWithChildren
+ NonNestedBazBazidEditRoute: typeof NonNestedBazBazidEditRoute
+}
+
+const NonNestedRouteRouteChildren: NonNestedRouteRouteChildren = {
+ NonNestedBazRoute: NonNestedBazRouteWithChildren,
+ NonNestedBazBazidEditRoute: NonNestedBazBazidEditRoute,
+}
+
+const NonNestedRouteRouteWithChildren = NonNestedRouteRoute._addFileChildren(
+ NonNestedRouteRouteChildren,
+)
+
interface SearchParamsRouteRouteChildren {
SearchParamsDefaultRoute: typeof SearchParamsDefaultRoute
SearchParamsIndexRoute: typeof SearchParamsIndexRoute
@@ -1363,6 +1466,7 @@ const ParamsPsNamedFooRouteRouteWithChildren =
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
+ NonNestedRouteRoute: NonNestedRouteRouteWithChildren,
SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren,
LayoutRoute: LayoutRouteWithChildren,
AnchorRoute: AnchorRoute,
diff --git a/e2e/solid-router/basic-file-based/src/routes/non-nested/baz.$bazid.tsx b/e2e/solid-router/basic-file-based/src/routes/non-nested/baz.$bazid.tsx
new file mode 100644
index 00000000000..d43c79c7807
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/non-nested/baz.$bazid.tsx
@@ -0,0 +1,23 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/non-nested/baz/$bazid')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const params = Route.useParams()
+
+ return (
+ <>
+
+ Hello "/non-nested/baz/$bazid"!
+
+
+ params:{' '}
+
+ {JSON.stringify(params())}
+
+
+ >
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/non-nested/baz.tsx b/e2e/solid-router/basic-file-based/src/routes/non-nested/baz.tsx
new file mode 100644
index 00000000000..42e76f8e3a0
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/non-nested/baz.tsx
@@ -0,0 +1,14 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/non-nested/baz')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+ <>
+ Hello "/non-nested/baz"!
+
+ >
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/non-nested/baz_.$bazid.edit.tsx b/e2e/solid-router/basic-file-based/src/routes/non-nested/baz_.$bazid.edit.tsx
new file mode 100644
index 00000000000..7cc0062d73c
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/non-nested/baz_.$bazid.edit.tsx
@@ -0,0 +1,22 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/non-nested/baz_/$bazid/edit')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const params = Route.useParams()
+ return (
+ <>
+
+ Hello "/non-nested/baz_/$bazid/edit"!
+
+
+ params:{' '}
+
+ {JSON.stringify(params())}
+
+
+ >
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/non-nested/route.tsx b/e2e/solid-router/basic-file-based/src/routes/non-nested/route.tsx
new file mode 100644
index 00000000000..f63bb147172
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/non-nested/route.tsx
@@ -0,0 +1,38 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/non-nested')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
+ Non-nested paths
+
+
+ -
+
+ /non-nested/baz/123
+
+
+ -
+
+ /non-nested/baz/456/edit
+
+
+
+
+
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/tests/app.spec.ts b/e2e/solid-router/basic-file-based/tests/app.spec.ts
index c0b1b69dc59..2b0ffa17a9b 100644
--- a/e2e/solid-router/basic-file-based/tests/app.spec.ts
+++ b/e2e/solid-router/basic-file-based/tests/app.spec.ts
@@ -296,3 +296,38 @@ test('Should remount deps when remountDeps does change ', async ({ page }) => {
'Page component mounts: 2',
)
})
+
+test('Should not nest non-nested paths', async ({ page }) => {
+ await page.goto('/non-nested')
+ await page.waitForURL('/non-nested')
+ const nonNestedPathHeading = page.getByTestId('non-nested-path-heading')
+ const bazIdLink = page.getByTestId('l-to-non-nested-bazid')
+ const bazIdEditLink = page.getByTestId('l-to-non-nested-bazid-edit')
+
+ await expect(nonNestedPathHeading).toBeInViewport()
+ await expect(bazIdLink).toHaveAttribute('href', '/non-nested/baz/123')
+ await expect(bazIdEditLink).toHaveAttribute(
+ 'href',
+ '/non-nested/baz/456/edit',
+ )
+
+ await bazIdLink.click()
+ await page.waitForURL('/non-nested/baz/123')
+ const bazHeading = page.getByTestId('non-nested-baz-heading')
+ const bazIdHeading = page.getByTestId('non-nested-bazid-heading')
+ const bazIdParam = page.getByTestId('non-nested-bazid-param')
+ await expect(nonNestedPathHeading).toBeInViewport()
+ await expect(bazHeading).toBeInViewport()
+ await expect(bazIdHeading).toBeInViewport()
+ await expect(bazIdParam).toContainText(JSON.stringify({ bazid: '123' }))
+
+ await bazIdEditLink.click()
+ await page.waitForURL('/non-nested/baz/456/edit')
+ const bazIdEditHeading = page.getByTestId('non-nested-bazid-edit-heading')
+ const bazIdEditParam = page.getByTestId('non-nested-bazid-edit-param')
+ await expect(nonNestedPathHeading).toBeInViewport()
+ await expect(bazHeading).toBeHidden()
+ await expect(bazIdHeading).toBeHidden()
+ await expect(bazIdEditHeading).toBeInViewport()
+ await expect(bazIdEditParam).toContainText(JSON.stringify({ bazid: '456' }))
+})
diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts
index ad8c5d2d565..d010cbb1ef9 100644
--- a/packages/router-core/src/path.ts
+++ b/packages/router-core/src/path.ts
@@ -259,8 +259,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]
@@ -273,7 +276,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) {
@@ -289,7 +292,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]
@@ -303,8 +306,8 @@ 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,
@@ -314,7 +317,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: '$',
@@ -326,12 +329,12 @@ 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')
- : decodeURI(part),
+ : decodeURI(partToMatch),
}
}),
)
diff --git a/packages/router-core/tests/match-by-path.test.ts b/packages/router-core/tests/match-by-path.test.ts
index 51466ab0d5f..8c582cfedfc 100644
--- a/packages/router-core/tests/match-by-path.test.ts
+++ b/packages/router-core/tests/match-by-path.test.ts
@@ -196,3 +196,191 @@ describe('fuzzy path matching', () => {
)
})
})
+
+describe('non-nested paths', () => {
+ describe('default path matching', () => {
+ it.each([
+ ['/', '/a_', '/a_', {}],
+ ['/', '/a_/b_', '/a_/b_', {}],
+ ['/', '/a_', '/a_/', {}],
+ ['/', '/a_/', '/a_/', {}],
+ ['/', '/a_/', '/a_', undefined],
+ ['/', '/b_', '/a_', undefined],
+ ])('static %s %s => %s', (base, from, to, result) => {
+ expect(
+ matchByPath(from, { to, caseSensitive: true, fuzzy: false }),
+ ).toEqual(result)
+ })
+
+ it.each([
+ ['/a/1', '/a_/$id_', { id: '1' }],
+ ['/a/1/b', '/a_/$id_/b_', { id: '1' }],
+ ['/a/1/b/2', '/a_/$id_/b_/$other_', { id: '1', other: '2' }],
+ ['/a/1/b/2', '/a_/$id_/b_/$id_', { id: '2' }],
+ ])('params %s => %s', (from, to, result) => {
+ expect(
+ matchByPath(from, { to, caseSensitive: true, fuzzy: false }),
+ ).toEqual(result)
+ })
+
+ it('params support more than alphanumeric characters', () => {
+ // in the value: basically everything except / and %
+ expect(
+ matchByPath(
+ '/a/@&é"\'(§è!çà)-_°^¨$*€£`ù=+:;.,?~<>|î©#0123456789\\😀}{',
+ { to: '/a_/$id_' },
+ ),
+ ).toEqual({
+ id: '@&é"\'(§è!çà)-_°^¨$*€£`ù=+:;.,?~<>|î©#0123456789\\😀}{',
+ })
+ // in the key: basically everything except / and % and $
+ expect(
+ matchByPath('/a/1', {
+ to: '/a_/$@&é"\'(§è!çà)-_°^¨*€£`ù=+:;.,?~<>|î©#0123456789\\😀}{_',
+ }),
+ ).toEqual({
+ '@&é"\'(§è!çà)-_°^¨*€£`ù=+:;.,?~<>|î©#0123456789\\😀}{': '1',
+ })
+ })
+
+ it.each([
+ ['/a/1', '/a_/{-$id}_', { id: '1' }],
+ ['/a', '/a_/{-$id}_', {}],
+ ['/a/1/b', '/a_/{-$id}_/b_', { id: '1' }],
+ ['/a/b', '/a_/{-$id}_/b_', {}],
+ ['/a/1/b/2', '/a_/{-$id}_/b_/{-$other}_', { id: '1', other: '2' }],
+ ['/a/b/2', '/a_/{-$id}_/b_/{-$other}_', { other: '2' }],
+ ['/a/1/b', '/a_/{-$id}_/b_/{-$other}_', { id: '1' }],
+ ['/a/b', '/a_/{-$id}_/b_/{-$other}_', {}],
+ ['/a/1/b/2', '/a_/{-$id}_/b_/{-$id}_', { id: '2' }],
+ ])('optional %s => %s', (from, to, result) => {
+ expect(
+ matchByPath(from, { to, caseSensitive: true, fuzzy: false }),
+ ).toEqual(result)
+ })
+
+ it.each([
+ ['/a/b/c', '/a_/$_', { _splat: 'b/c', '*': 'b/c' }],
+ ['/a/', '/a_/$_', { _splat: '/', '*': '/' }],
+ ['/a', '/a_/$_', { _splat: '', '*': '' }],
+ ['/a/b/c', '/a_/$_/foo_', { _splat: 'b/c', '*': 'b/c' }],
+ ])('wildcard %s => %s', (from, to, result) => {
+ expect(
+ matchByPath(from, { to, caseSensitive: true, fuzzy: false }),
+ ).toEqual(result)
+ })
+ })
+
+ describe('case insensitive path matching', () => {
+ it.each([
+ ['/', '/a', '/A_', {}],
+ ['/', '/a/b', '/A_/B_', {}],
+ ['/', '/a', '/A_/', {}],
+ ['/', '/a/', '/A_/', {}],
+ ['/', '/a/', '/A_', undefined],
+ ['/', '/b', '/A_', undefined],
+ ])('static %s %s => %s', (base, from, to, result) => {
+ expect(
+ matchByPath(from, { to, caseSensitive: false, fuzzy: false }),
+ ).toEqual(result)
+ })
+
+ it.each([
+ ['/a/1', '/A_/$id_', { id: '1' }],
+ ['/a/1/b', '/A_/$id_/B_', { id: '1' }],
+ ['/a/1/b/2', '/A_/$id_/B_/$other_', { id: '1', other: '2' }],
+ ['/a/1/b/2', '/A_/$id_/B_/$id_', { id: '2' }],
+ ])('params %s => %s', (from, to, result) => {
+ expect(
+ matchByPath(from, { to, caseSensitive: false, fuzzy: false }),
+ ).toEqual(result)
+ })
+
+ it.each([
+ ['/a/1', '/A_/{-$id}_', { id: '1' }],
+ ['/a', '/A_/{-$id}_', {}],
+ ['/a/1/b', '/A_/{-$id}_/B_', { id: '1' }],
+ ['/a/1/b/2', '/A_/{-$id}_/B_/{-$other}_', { id: '1', other: '2' }],
+ ['/a/1/b', '/A_/{-$id}_/B_/{-$other}_', { id: '1' }],
+ ['/a/1/b/2', '/A_/{-$id}_/B/{-$id}_', { id: '2' }],
+ ])('optional %s => %s', (from, to, result) => {
+ expect(
+ matchByPath(from, { to, caseSensitive: false, fuzzy: false }),
+ ).toEqual(result)
+ })
+
+ it.each([
+ ['/a/b/c', '/A_/$_', { _splat: 'b/c', '*': 'b/c' }],
+ ['/a/', '/A_/$_', { _splat: '/', '*': '/' }],
+ ['/a', '/A_/$_', { _splat: '', '*': '' }],
+ ['/a/b/c', '/A_/$_/foo_', { _splat: 'b/c', '*': 'b/c' }],
+ ])('wildcard %s => %s', (from, to, result) => {
+ expect(
+ matchByPath(from, { to, caseSensitive: false, fuzzy: false }),
+ ).toEqual(result)
+ })
+ })
+
+ describe('fuzzy path matching', () => {
+ it.each([
+ ['/', '/a', '/a_', {}],
+ ['/', '/a', '/a_/', {}],
+ ['/', '/a/', '/a_/', {}],
+ ['/', '/a/', '/a_', { '**': '/' }],
+ ['/', '/a/b', '/a_/b_', {}],
+ ['/', '/a/b', '/a_', { '**': 'b' }],
+ ['/', '/a/b/', '/a_', { '**': 'b/' }],
+ ['/', '/a/b/c', '/a_', { '**': 'b/c' }],
+ ['/', '/a', '/a_/b_', undefined],
+ ['/', '/b', '/a_', undefined],
+ ['/', '/a', '/b_', undefined],
+ ])('static %s %s => %s', (base, from, to, result) => {
+ expect(
+ matchByPath(from, { to, fuzzy: true, caseSensitive: true }),
+ ).toEqual(result)
+ })
+
+ it.each([
+ ['/a/1', '/a_/$id_', { id: '1' }],
+ ['/a/1/b', '/a_/$id_', { id: '1', '**': 'b' }],
+ ['/a/1/', '/a_/$id_/', { id: '1' }],
+ ['/a/1/b/2', '/a_/$id_/b_/$other_', { id: '1', other: '2' }],
+ ['/a/1/b/2/c', '/a_/$id_/b_/$other_', { id: '1', other: '2', '**': 'c' }],
+ ])('params %s => %s', (from, to, result) => {
+ expect(
+ matchByPath(from, { to, fuzzy: true, caseSensitive: true }),
+ ).toEqual(result)
+ })
+
+ it.each([
+ ['/a/1', '/a_/{-$id}_', { id: '1' }],
+ ['/a', '/a_/{-$id}_', {}],
+ ['/a/1/b', '/a_/{-$id}_', { '**': 'b', id: '1' }],
+ ['/a/1/b', '/a_/{-$id}_/b_', { id: '1' }],
+ ['/a/b', '/a_/{-$id}_/b_', {}],
+ ['/a/b/c', '/a_/{-$id}_/b_', { '**': 'c' }],
+ ['/a/b', '/a/_{-$id}_/b_/{-$other}_', {}],
+ ['/a/b/2/d', '/a_/{-$id}_/b_/{-$other}_', { other: '2', '**': 'd' }],
+ [
+ '/a/1/b/2/c',
+ '/a_/{-$id}_/b_/{-$other}_',
+ { id: '1', other: '2', '**': 'c' },
+ ],
+ ])('optional %s => %s', (from, to, result) => {
+ expect(
+ matchByPath(from, { to, fuzzy: true, caseSensitive: true }),
+ ).toEqual(result)
+ })
+
+ it.each([
+ ['/a/b/c', '/a_/$_', { _splat: 'b/c', '*': 'b/c' }],
+ ['/a/', '/a_/$_', { _splat: '/', '*': '/' }],
+ ['/a', '/a_/$_', { _splat: '', '*': '' }],
+ ['/a/b/c/d', '/a_/$_/foo_', { _splat: 'b/c/d', '*': 'b/c/d' }],
+ ])('wildcard %s => %s', (from, to, result) => {
+ expect(
+ matchByPath(from, { to, fuzzy: true, caseSensitive: true }),
+ ).toEqual(result)
+ })
+ })
+})
diff --git a/packages/router-core/tests/path.test.ts b/packages/router-core/tests/path.test.ts
index 96458b4259b..0f4b362d77f 100644
--- a/packages/router-core/tests/path.test.ts
+++ b/packages/router-core/tests/path.test.ts
@@ -112,6 +112,7 @@ describe('resolvePath', () => {
expect(resolvePath({ base: a + '/', to: b + '/' })).toEqual(eq)
})
})
+
describe('trailingSlash', () => {
describe(`'always'`, () => {
it('keeps trailing slash', () => {
@@ -973,3 +974,920 @@ describe('parsePathname', () => {
})
})
})
+
+describe('non-nested paths', async () => {
+ describe('resolvePath', () => {
+ describe.each([
+ ['/', '/a_', '/a'],
+ ['/', 'a_/', '/a'],
+ ['/', '/a_/b_', '/a/b'],
+ ['/a', 'b_', '/a/b'],
+ ['/', 'a_/b_', '/a/b'],
+ ['/', './a_/b_', '/a/b'],
+ ['/a/b/c', 'd_', '/a/b/c/d'],
+ ['/a/b/c', './d_', '/a/b/c/d'],
+ ['/a/b/c', './../d_', '/a/b/d'],
+ ['/a/b/c/d', './../d_', '/a/b/c/d'],
+ ['/a/b/c', '../../d_', '/a/d'],
+ ['/a/b/c', '../d_', '/a/b/d'],
+ ])('resolves correctly', (a, b, eq) => {
+ it(`${a} to ${b} === ${eq}`, () => {
+ expect(resolvePath({ base: a, to: b })).toEqual(eq)
+ })
+ it(`${a}/ to ${b} === ${eq} (trailing slash)`, () => {
+ expect(resolvePath({ base: a + '/', to: b })).toEqual(eq)
+ })
+ it(`${a}/ to ${b}/ === ${eq} (trailing slash + trailing slash)`, () => {
+ expect(resolvePath({ base: a + '/', to: b + '/' })).toEqual(eq)
+ })
+ })
+
+ describe('trailingSlash', () => {
+ describe(`'always'`, () => {
+ it('keeps trailing slash', () => {
+ expect(
+ resolvePath({
+ base: '/a/b/c',
+ to: 'd_/',
+ trailingSlash: 'always',
+ }),
+ ).toBe('/a/b/c/d/')
+ })
+ it('adds trailing slash', () => {
+ expect(
+ resolvePath({
+ base: '/a/b/c',
+ to: 'd_',
+ trailingSlash: 'always',
+ }),
+ ).toBe('/a/b/c/d/')
+ })
+ })
+
+ describe(`'never'`, () => {
+ it('removes trailing slash', () => {
+ expect(
+ resolvePath({
+ base: '/a/b/c',
+ to: 'd_/',
+ trailingSlash: 'never',
+ }),
+ ).toBe('/a/b/c/d')
+ })
+ it('does not add trailing slash', () => {
+ expect(
+ resolvePath({
+ base: '/a/b/c',
+ to: 'd_',
+ trailingSlash: 'never',
+ }),
+ ).toBe('/a/b/c/d')
+ })
+ })
+
+ describe(`'preserve'`, () => {
+ it('keeps trailing slash', () => {
+ expect(
+ resolvePath({
+ base: '/a/b/c',
+ to: 'd_/',
+ trailingSlash: 'preserve',
+ }),
+ ).toBe('/a/b/c/d/')
+ })
+ it('does not add trailing slash', () => {
+ expect(
+ resolvePath({
+ base: '/a/b/c',
+ to: 'd_',
+ trailingSlash: 'preserve',
+ }),
+ ).toBe('/a/b/c/d')
+ })
+ })
+ })
+
+ describe.each([{ base: '/' }, { base: '/nested' }])(
+ 'param routes w/ base=$base',
+ ({ base }) => {
+ describe('wildcard (prefix + suffix)', () => {
+ it.each([
+ { name: 'regular top-level', to: '/$_', expected: '/$' },
+ {
+ name: 'regular nested',
+ to: '/params/wildcard_/$_',
+ expected: '/params/wildcard/$',
+ },
+ {
+ name: 'with top-level prefix',
+ to: '/prefix{$}_',
+ expected: '/prefix{$}',
+ },
+ {
+ name: 'with nested prefix',
+ to: '/params_/wildcard/prefix{$}_',
+ expected: '/params/wildcard/prefix{$}',
+ },
+ {
+ name: 'with top-level suffix',
+ to: '/{$}suffix_',
+ expected: '/{$}suffix',
+ },
+ {
+ name: 'with nested suffix',
+ to: '/params_/wildcard_/{$}suffix_',
+ expected: '/params/wildcard/{$}suffix',
+ },
+ {
+ name: 'with top-level prefix + suffix',
+ to: '/prefix{$}suffix_',
+ expected: '/prefix{$}suffix',
+ },
+ {
+ name: 'with nested prefix + suffix',
+ to: '/params/wildcard/prefix{$}suffix_',
+ expected: '/params/wildcard/prefix{$}suffix',
+ },
+ ])('$name', ({ to, expected }) => {
+ const candidate = base + trimPathLeft(to)
+ const result = base + trimPathLeft(expected)
+ expect(
+ resolvePath({
+ base,
+ to: candidate,
+ trailingSlash: 'never',
+ }),
+ ).toEqual(result)
+ })
+ })
+
+ describe('named (prefix + suffix)', () => {
+ it.each([
+ { name: 'regular top-level', to: '/$foo_', expected: '/$foo' },
+ {
+ name: 'regular nested',
+ to: '/params/named_/$foo_',
+ expected: '/params/named/$foo',
+ },
+ {
+ name: 'with top-level prefix',
+ to: '/prefix{$foo}_',
+ expected: '/prefix{$foo}',
+ },
+ {
+ name: 'with nested prefix',
+ to: '/params_/named/prefix{$foo}_',
+ expected: '/params/named/prefix{$foo}',
+ },
+ {
+ name: 'with top-level suffix',
+ to: '/{$foo}suffix_',
+ expected: '/{$foo}suffix',
+ },
+ {
+ name: 'with nested suffix',
+ to: '/params_/named_/{$foo}suffix_',
+ expected: '/params/named/{$foo}suffix',
+ },
+ {
+ name: 'with top-level prefix + suffix',
+ to: '/prefix{$foo}suffix_',
+ expected: '/prefix{$foo}suffix',
+ },
+ {
+ name: 'with nested prefix + suffix',
+ to: '/params_/named_/prefix{$foo}suffix_',
+ expected: '/params/named/prefix{$foo}suffix',
+ },
+ ])('$name', ({ to, expected }) => {
+ const candidate = base + trimPathLeft(to)
+ const result = base + trimPathLeft(expected)
+ expect(
+ resolvePath({
+ base,
+ to: candidate,
+ trailingSlash: 'never',
+ }),
+ ).toEqual(result)
+ })
+ })
+ },
+ )
+ })
+
+ describe('interpolatePath', () => {
+ describe('regular usage', () => {
+ it.each([
+ {
+ name: 'should interpolate the path',
+ path: '/users_/$id_',
+ params: { id: '123' },
+ result: '/users/123',
+ },
+ {
+ name: 'should interpolate the path with multiple params',
+ path: '/users/$id_/$name_',
+ params: { id: '123', name: 'tanner' },
+ result: '/users/123/tanner',
+ },
+ {
+ name: 'should interpolate the path with extra params',
+ path: '/users/$id_',
+ params: { id: '123', name: 'tanner' },
+ result: '/users/123',
+ },
+ {
+ name: 'should interpolate the path with missing params',
+ path: '/users/$id/$name_',
+ params: { id: '123' },
+ result: '/users/123/undefined',
+ },
+ {
+ name: 'should interpolate the path with missing params and extra params',
+ path: '/users/$id_',
+ params: { name: 'john' },
+ result: '/users/undefined',
+ },
+ {
+ name: 'should interpolate the path with the param being a number',
+ path: '/users/$id_',
+ params: { id: 123 },
+ result: '/users/123',
+ },
+ {
+ name: 'should interpolate the path with the param being a falsey number',
+ path: '/users/$id_',
+ params: { id: 0 },
+ result: '/users/0',
+ },
+ {
+ name: 'should interpolate the path with URI component encoding',
+ path: '/users/$id_',
+ params: { id: '?#@john+smith' },
+ result: '/users/%3F%23%40john%2Bsmith',
+ },
+ {
+ name: 'should interpolate the path without URI encoding characters in decodeCharMap',
+ path: '/users/$id_',
+ params: { id: '?#@john+smith' },
+ result: '/users/%3F%23@john+smith',
+ decodeCharMap: new Map(
+ ['@', '+'].map((char) => [encodeURIComponent(char), char]),
+ ),
+ },
+ {
+ name: 'should interpolate the path with the splat param at the end',
+ path: '/users/$_',
+ params: { _splat: '123' },
+ result: '/users/123',
+ },
+ {
+ name: 'should interpolate the path with a single named path param and the splat param at the end',
+ path: '/users/$username/$_',
+ params: { username: 'seancassiere', _splat: '123' },
+ result: '/users/seancassiere/123',
+ },
+ {
+ name: 'should interpolate the path with 2 named path params with the splat param at the end',
+ path: '/users/$username/$id_/$_',
+ params: { username: 'seancassiere', id: '123', _splat: '456' },
+ result: '/users/seancassiere/123/456',
+ },
+ {
+ name: 'should interpolate the path with multiple named path params with the splat param at the end',
+ path: '/$username/settings_/$repo_/$id_/$_',
+ params: {
+ username: 'sean-cassiere',
+ repo: 'my-repo',
+ id: '123',
+ _splat: '456',
+ },
+ result: '/sean-cassiere/settings/my-repo/123/456',
+ },
+ {
+ name: 'should interpolate the path with the splat param containing slashes',
+ path: '/users_/$',
+ params: { _splat: 'sean/cassiere' },
+ result: '/users/sean/cassiere',
+ },
+ ])('$name', ({ path, params, decodeCharMap, result }) => {
+ expect(
+ interpolatePath({
+ path,
+ params,
+ decodeCharMap,
+ }).interpolatedPath,
+ ).toBe(result)
+ })
+ })
+
+ describe('wildcard (prefix + suffix)', () => {
+ it.each([
+ {
+ name: 'regular',
+ to: '/$_',
+ params: { _splat: 'bar/foo/me' },
+ result: '/bar/foo/me',
+ },
+ {
+ name: 'regular curly braces',
+ to: '/{$}_',
+ params: { _splat: 'bar/foo/me' },
+ result: '/bar/foo/me',
+ },
+ {
+ name: 'with prefix',
+ to: '/prefix{$}_',
+ params: { _splat: 'bar' },
+ result: '/prefixbar',
+ },
+ {
+ name: 'with suffix',
+ to: '/{$}-suffix_',
+ params: { _splat: 'bar' },
+ result: '/bar-suffix',
+ },
+ {
+ name: 'with prefix + suffix',
+ to: '/prefix{$}-suffix_',
+ params: { _splat: 'bar' },
+ result: '/prefixbar-suffix',
+ },
+ ])('$name', ({ to, params, result }) => {
+ expect(
+ interpolatePath({
+ path: to,
+ params,
+ }).interpolatedPath,
+ ).toBe(result)
+ })
+ })
+
+ describe('named params (prefix + suffix)', () => {
+ it.each([
+ {
+ name: 'regular',
+ to: '/$foo_',
+ params: { foo: 'bar' },
+ result: '/bar',
+ },
+ {
+ name: 'regular curly braces',
+ to: '/{$foo}_',
+ params: { foo: 'bar' },
+ result: '/bar',
+ },
+ {
+ name: 'with prefix',
+ to: '/prefix{$bar}_',
+ params: { bar: 'baz' },
+ result: '/prefixbaz',
+ },
+ {
+ name: 'with suffix',
+ to: '/{$foo}.suffix_',
+ params: { foo: 'bar' },
+ result: '/bar.suffix',
+ },
+ {
+ name: 'with prefix and suffix',
+ to: '/prefix{$param}.suffix_',
+ params: { param: 'foobar' },
+ result: '/prefixfoobar.suffix',
+ },
+ ])('$name', ({ to, params, result }) => {
+ expect(
+ interpolatePath({
+ path: to,
+ params,
+ }).interpolatedPath,
+ ).toBe(result)
+ })
+ })
+
+ describe('should handle missing _splat parameter for', () => {
+ it.each([
+ {
+ name: 'basic splat route',
+ path: '/hello/$_',
+ params: {},
+ expectedResult: '/hello',
+ },
+ {
+ name: 'splat route with prefix',
+ path: '/hello_/prefix{$}_',
+ params: {},
+ expectedResult: '/hello/prefix',
+ },
+ {
+ name: 'splat route with suffix',
+ path: '/hello/{$}suffix_',
+ params: {},
+ expectedResult: '/hello/suffix',
+ },
+ {
+ name: 'splat route with prefix and suffix',
+ path: '/hello/prefix{$}suffix_',
+ params: {},
+ expectedResult: '/hello/prefixsuffix',
+ },
+ ])('$name', ({ path, params, expectedResult }) => {
+ const result = interpolatePath({
+ path,
+ params,
+ })
+ expect(result.interpolatedPath).toBe(expectedResult)
+ expect(result.isMissingParams).toBe(true)
+ })
+ })
+ })
+
+ describe('matchPathname', () => {
+ describe('path param(s) matching', () => {
+ it.each([
+ {
+ name: 'should not match since `to` does not match the input',
+ input: '/',
+ matchingOptions: {
+ to: '/users_',
+ },
+ expectedMatchedParams: undefined,
+ },
+ {
+ name: 'should match since `to` matches the input',
+ input: '/users',
+ matchingOptions: {
+ to: '/users_',
+ },
+ expectedMatchedParams: {},
+ },
+ {
+ name: 'should match and return the 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',
+ matchingOptions: {
+ to: '/users/$_',
+ },
+ expectedMatchedParams: {
+ '*': '123',
+ _splat: '123',
+ },
+ },
+ {
+ name: 'should match and return the named path and splat params',
+ input: '/users/123/456',
+ matchingOptions: {
+ to: '/users_/$id_/$_',
+ },
+ expectedMatchedParams: {
+ id: '123',
+ '*': '456',
+ _splat: '456',
+ },
+ },
+ {
+ name: 'should match and return the multiple named path params and splat param',
+ input: '/sean-cassiere/settings/my-repo/123/456',
+ matchingOptions: {
+ to: '/$username_/settings_/$repo_/$id_/$_',
+ },
+ expectedMatchedParams: {
+ username: 'sean-cassiere',
+ repo: 'my-repo',
+ id: '123',
+ '*': '456',
+ _splat: '456',
+ },
+ },
+ {
+ name: 'should match and return the splat params when multiple subsequent segments are present',
+ input: '/docs/tanner/sean/manuel',
+ matchingOptions: {
+ to: '/docs/$_',
+ },
+ expectedMatchedParams: {
+ '*': 'tanner/sean/manuel',
+ _splat: 'tanner/sean/manuel',
+ },
+ },
+ ])('$name', ({ input, matchingOptions, expectedMatchedParams }) => {
+ expect(matchPathname(input, matchingOptions)).toStrictEqual(
+ expectedMatchedParams,
+ )
+ })
+ })
+
+ describe('wildcard (prefix + suffix)', () => {
+ it.each([
+ {
+ name: 'regular',
+ input: '/docs/foo/bar',
+ matchingOptions: {
+ to: '/docs/$_',
+ },
+ expectedMatchedParams: {
+ '*': 'foo/bar',
+ _splat: 'foo/bar',
+ },
+ },
+ {
+ name: 'regular curly braces',
+ input: '/docs/foo/bar',
+ matchingOptions: {
+ to: '/docs_/{$}_',
+ },
+ expectedMatchedParams: {
+ '*': 'foo/bar',
+ _splat: 'foo/bar',
+ },
+ },
+ {
+ name: 'with prefix',
+ input: '/docs/prefixbar/baz',
+ matchingOptions: {
+ to: '/docs_/prefix{$}_',
+ },
+ expectedMatchedParams: {
+ '*': 'bar/baz',
+ _splat: 'bar/baz',
+ },
+ },
+ {
+ name: 'with suffix',
+ input: '/docs/bar/baz.suffix',
+ matchingOptions: {
+ to: '/docs/{$}.suffix_',
+ },
+ expectedMatchedParams: {
+ '*': 'bar/baz',
+ _splat: 'bar/baz',
+ },
+ },
+ {
+ name: 'with prefix + suffix',
+ input: '/docs/prefixbar/baz-suffix',
+ matchingOptions: {
+ to: '/docs/prefix{$}-suffix_',
+ },
+ expectedMatchedParams: {
+ '*': 'bar/baz',
+ _splat: 'bar/baz',
+ },
+ },
+ ])('$name', ({ input, matchingOptions, expectedMatchedParams }) => {
+ expect(matchPathname(input, matchingOptions)).toStrictEqual(
+ expectedMatchedParams,
+ )
+ })
+ })
+
+ describe('named params (prefix + suffix)', () => {
+ it.each([
+ {
+ name: 'regular',
+ input: '/docs/foo',
+ matchingOptions: {
+ to: '/docs/$bar_',
+ },
+ expectedMatchedParams: {
+ bar: 'foo',
+ },
+ },
+ {
+ name: 'regular curly braces',
+ input: '/docs/foo',
+ matchingOptions: {
+ to: '/docs/{$bar}_',
+ },
+ expectedMatchedParams: {
+ bar: 'foo',
+ },
+ },
+ {
+ name: 'with prefix',
+ input: '/docs/prefixfoo',
+ matchingOptions: {
+ to: '/docs/prefix{$bar}_',
+ },
+ expectedMatchedParams: {
+ bar: 'foo',
+ },
+ },
+ {
+ name: 'with suffix',
+ input: '/docs/foo.suffix',
+ matchingOptions: {
+ to: '/docs/{$bar}.suffix_',
+ },
+ expectedMatchedParams: {
+ bar: 'foo',
+ },
+ },
+ {
+ name: 'with prefix + suffix',
+ input: '/docs/prefixfoobar-suffix',
+ matchingOptions: {
+ to: '/docs/prefix{$param}-suffix_',
+ },
+ expectedMatchedParams: {
+ param: 'foobar',
+ },
+ },
+ ])('$name', ({ input, matchingOptions, expectedMatchedParams }) => {
+ expect(matchPathname(input, matchingOptions)).toStrictEqual(
+ expectedMatchedParams,
+ )
+ })
+ })
+ })
+
+ describe('parsePathname', () => {
+ type ParsePathnameTestScheme = Array<{
+ name: string
+ to: string | undefined
+ expected: Array
+ }>
+
+ describe('regular usage', () => {
+ it.each([
+ {
+ name: 'should handle pathname with a single segment',
+ to: '/foo_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ { type: SEGMENT_TYPE_PATHNAME, value: 'foo' },
+ ],
+ },
+ {
+ name: 'should handle pathname with multiple segments',
+ to: '/foo_/bar_/baz_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ { type: SEGMENT_TYPE_PATHNAME, value: 'foo' },
+ { type: SEGMENT_TYPE_PATHNAME, value: 'bar' },
+ { type: SEGMENT_TYPE_PATHNAME, value: 'baz' },
+ ],
+ },
+ {
+ name: 'should handle pathname with a trailing slash',
+ to: '/foo_/',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ { type: SEGMENT_TYPE_PATHNAME, value: 'foo' },
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ ],
+ },
+ {
+ name: 'should handle 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_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ { type: SEGMENT_TYPE_PARAM, value: '$bar' },
+ ],
+ },
+ {
+ name: 'should handle named params followed by a segment',
+ to: '/foo_/$bar_/baz_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ { type: SEGMENT_TYPE_PATHNAME, value: 'foo' },
+ { type: SEGMENT_TYPE_PARAM, value: '$bar' },
+ { type: SEGMENT_TYPE_PATHNAME, value: 'baz' },
+ ],
+ },
+ {
+ name: 'should handle multiple named params',
+ to: '/foo_/$bar_/$baz_/qux_/$quux_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ { type: SEGMENT_TYPE_PATHNAME, value: 'foo' },
+ { type: SEGMENT_TYPE_PARAM, value: '$bar' },
+ { type: SEGMENT_TYPE_PARAM, value: '$baz' },
+ { type: SEGMENT_TYPE_PATHNAME, value: 'qux' },
+ { type: SEGMENT_TYPE_PARAM, value: '$quux' },
+ ],
+ },
+ {
+ name: 'should handle splat params',
+ to: '/foo_/$_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ { type: SEGMENT_TYPE_PATHNAME, value: 'foo' },
+ { type: SEGMENT_TYPE_WILDCARD, value: '$' },
+ ],
+ },
+ {
+ name: 'should handle splat params at the root',
+ to: '/$_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ { type: SEGMENT_TYPE_WILDCARD, value: '$' },
+ ],
+ },
+ ] satisfies ParsePathnameTestScheme)('$name', ({ to, expected }) => {
+ const result = parsePathname(to)
+ expect(result).toEqual(expected)
+ })
+ })
+
+ describe('wildcard (prefix + suffix)', () => {
+ it.each([
+ {
+ name: 'regular',
+ to: '/$_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ { type: SEGMENT_TYPE_WILDCARD, value: '$' },
+ ],
+ },
+ {
+ name: 'regular curly braces',
+ to: '/{$}_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ { type: SEGMENT_TYPE_WILDCARD, value: '$' },
+ ],
+ },
+ {
+ name: 'with prefix (regular text)',
+ to: '/foo{$}_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ {
+ type: SEGMENT_TYPE_WILDCARD,
+ value: '$',
+ prefixSegment: 'foo',
+ },
+ ],
+ },
+ {
+ name: 'with prefix + followed by special character',
+ to: '/foo.{$}_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ {
+ type: SEGMENT_TYPE_WILDCARD,
+ value: '$',
+ prefixSegment: 'foo.',
+ },
+ ],
+ },
+ {
+ name: 'with suffix',
+ to: '/{$}-foo_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ {
+ type: SEGMENT_TYPE_WILDCARD,
+ value: '$',
+ suffixSegment: '-foo',
+ },
+ ],
+ },
+ {
+ name: 'with prefix + suffix',
+ to: '/foo{$}-bar_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ {
+ type: SEGMENT_TYPE_WILDCARD,
+ value: '$',
+ prefixSegment: 'foo',
+ suffixSegment: '-bar',
+ },
+ ],
+ },
+ {
+ name: 'with prefix + followed by special character and a segment',
+ to: '/foo.{$}_/bar_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ {
+ type: SEGMENT_TYPE_WILDCARD,
+ value: '$',
+ prefixSegment: 'foo.',
+ },
+ { type: SEGMENT_TYPE_PATHNAME, value: 'bar' },
+ ],
+ },
+ ] satisfies ParsePathnameTestScheme)('$name', ({ to, expected }) => {
+ const result = parsePathname(to)
+ expect(result).toEqual(expected)
+ })
+ })
+
+ describe('named params (prefix + suffix)', () => {
+ it.each([
+ {
+ name: 'regular',
+ to: '/$bar_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ { type: SEGMENT_TYPE_PARAM, value: '$bar' },
+ ],
+ },
+ {
+ name: 'regular curly braces',
+ to: '/{$bar}_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ { type: SEGMENT_TYPE_PARAM, value: '$bar' },
+ ],
+ },
+ {
+ name: 'with prefix (regular text)',
+ to: '/foo{$bar}_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ {
+ type: SEGMENT_TYPE_PARAM,
+ value: '$bar',
+ prefixSegment: 'foo',
+ },
+ ],
+ },
+ {
+ name: 'with prefix + followed by special character',
+ to: '/foo.{$bar}_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ {
+ type: SEGMENT_TYPE_PARAM,
+ value: '$bar',
+ prefixSegment: 'foo.',
+ },
+ ],
+ },
+ {
+ name: 'with suffix',
+ to: '/{$bar}.foo_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ {
+ type: SEGMENT_TYPE_PARAM,
+ value: '$bar',
+ suffixSegment: '.foo',
+ },
+ ],
+ },
+ {
+ name: 'with suffix + started by special character',
+ to: '/{$bar}.foo_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ {
+ type: SEGMENT_TYPE_PARAM,
+ value: '$bar',
+ suffixSegment: '.foo',
+ },
+ ],
+ },
+ {
+ name: 'with suffix + started by special character and followed by segment',
+ to: '/{$bar}.foo_/baz_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ {
+ type: SEGMENT_TYPE_PARAM,
+ value: '$bar',
+ suffixSegment: '.foo',
+ },
+ { type: SEGMENT_TYPE_PATHNAME, value: 'baz' },
+ ],
+ },
+ {
+ name: 'with suffix + prefix',
+ to: '/foo{$bar}.baz_',
+ expected: [
+ { type: SEGMENT_TYPE_PATHNAME, value: '/' },
+ {
+ type: SEGMENT_TYPE_PARAM,
+ value: '$bar',
+ prefixSegment: 'foo',
+ suffixSegment: '.baz',
+ },
+ ],
+ },
+ ] satisfies ParsePathnameTestScheme)('$name', ({ to, expected }) => {
+ const result = parsePathname(to)
+ expect(result).toEqual(expected)
+ })
+ })
+ })
+})