From c12d3a497dcce88decdf93c91bc279635d160b38 Mon Sep 17 00:00:00 2001 From: Nico Lynzaad Date: Fri, 19 Sep 2025 01:42:34 +0200 Subject: [PATCH 1/2] merge #5165 and #5169 with alpha --- .../basic-file-based/src/routeTree.gen.ts | 104 +++ .../src/routes/non-nested/baz.$bazid.tsx | 23 + .../src/routes/non-nested/baz.tsx | 14 + .../routes/non-nested/baz_.$bazid.edit.tsx | 22 + .../src/routes/non-nested/route.tsx | 38 + .../basic-file-based/tests/app.spec.ts | 35 + .../basic-file-based/src/routeTree.gen.ts | 104 +++ .../src/routes/non-nested/baz.$bazid.tsx | 23 + .../src/routes/non-nested/baz.tsx | 14 + .../routes/non-nested/baz_.$bazid.edit.tsx | 22 + .../src/routes/non-nested/route.tsx | 38 + .../basic-file-based/tests/app.spec.ts | 35 + packages/router-core/src/path.ts | 21 +- .../router-core/tests/match-by-path.test.ts | 189 ++++ packages/router-core/tests/path.test.ts | 878 ++++++++++++++++++ 15 files changed, 1551 insertions(+), 9 deletions(-) create mode 100644 e2e/react-router/basic-file-based/src/routes/non-nested/baz.$bazid.tsx create mode 100644 e2e/react-router/basic-file-based/src/routes/non-nested/baz.tsx create mode 100644 e2e/react-router/basic-file-based/src/routes/non-nested/baz_.$bazid.edit.tsx create mode 100644 e2e/react-router/basic-file-based/src/routes/non-nested/route.tsx create mode 100644 e2e/solid-router/basic-file-based/src/routes/non-nested/baz.$bazid.tsx create mode 100644 e2e/solid-router/basic-file-based/src/routes/non-nested/baz.tsx create mode 100644 e2e/solid-router/basic-file-based/src/routes/non-nested/baz_.$bazid.edit.tsx create mode 100644 e2e/solid-router/basic-file-based/src/routes/non-nested/route.tsx 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..739df64ce5a 100644 --- a/packages/router-core/tests/match-by-path.test.ts +++ b/packages/router-core/tests/match-by-path.test.ts @@ -196,3 +196,192 @@ 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..83a577d3939 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,880 @@ 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) + }) + }) + }) +}) From 58c84f3503db1edad939f9328a466cd4670194e3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 23:46:25 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- .../router-core/tests/match-by-path.test.ts | 3 +- packages/router-core/tests/path.test.ts | 60 +++++++++++++++---- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/packages/router-core/tests/match-by-path.test.ts b/packages/router-core/tests/match-by-path.test.ts index 739df64ce5a..8c582cfedfc 100644 --- a/packages/router-core/tests/match-by-path.test.ts +++ b/packages/router-core/tests/match-by-path.test.ts @@ -197,7 +197,6 @@ describe('fuzzy path matching', () => { }) }) - describe('non-nested paths', () => { describe('default path matching', () => { it.each([ @@ -220,7 +219,7 @@ describe('non-nested paths', () => { ['/a/1/b/2', '/a_/$id_/b_/$id_', { id: '2' }], ])('params %s => %s', (from, to, result) => { expect( - matchByPath( from, { to, caseSensitive: true, fuzzy: false }), + matchByPath(from, { to, caseSensitive: true, fuzzy: false }), ).toEqual(result) }) diff --git a/packages/router-core/tests/path.test.ts b/packages/router-core/tests/path.test.ts index 83a577d3939..0f4b362d77f 100644 --- a/packages/router-core/tests/path.test.ts +++ b/packages/router-core/tests/path.test.ts @@ -1073,11 +1073,31 @@ describe('non-nested paths', async () => { 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: '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_', @@ -1104,11 +1124,31 @@ describe('non-nested paths', async () => { 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: '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_',