diff --git a/.changeset/five-badgers-listen.md b/.changeset/five-badgers-listen.md new file mode 100644 index 00000000000..da4adb5f6f1 --- /dev/null +++ b/.changeset/five-badgers-listen.md @@ -0,0 +1,5 @@ +--- +'@tanstack/router-generator': patch +--- + +Fix malformed generated paths when a `physical()` mount points at a subtree rooted by `__virtual.ts`, including nested virtual layouts that mount additional physical routes. diff --git a/packages/router-generator/src/filesystem/physical/getRouteNodes.ts b/packages/router-generator/src/filesystem/physical/getRouteNodes.ts index 8d25e7b71a7..493def044eb 100644 --- a/packages/router-generator/src/filesystem/physical/getRouteNodes.ts +++ b/packages/router-generator/src/filesystem/physical/getRouteNodes.ts @@ -1,6 +1,7 @@ import path from 'node:path' import * as fsp from 'node:fs/promises' import { + cleanPath, determineInitialRoutePath, escapeRegExp, hasEscapedLeadingUnderscore, @@ -121,16 +122,21 @@ export async function getRouteNodes( ) allPhysicalDirectories.push(...physicalDirectories) virtualRouteNodes.forEach((node) => { - const filePath = replaceBackslash(path.join(dir, node.filePath)) - const routePath = `/${dir}${node.routePath}` + const normalizedDir = dir === './' ? '' : dir + const filePath = replaceBackslash( + path.join(normalizedDir, node.filePath), + ) + const routePath = cleanPath(`/${normalizedDir}${node.routePath}`) node.variableName = routePathToVariable( - `${dir}/${removeExt(node.filePath)}`, + cleanPath(`/${normalizedDir}/${removeExt(node.filePath)}`), ) node.routePath = routePath // Keep originalRoutePath aligned with routePath for escape detection if (node.originalRoutePath) { - node.originalRoutePath = `/${dir}${node.originalRoutePath}` + node.originalRoutePath = cleanPath( + `/${normalizedDir}${node.originalRoutePath}`, + ) } node.filePath = filePath // Virtual subtree nodes (from __virtual.ts) are embedded in a diff --git a/packages/router-generator/src/filesystem/virtual/getRouteNodes.ts b/packages/router-generator/src/filesystem/virtual/getRouteNodes.ts index 3a603d57690..c477f8a004c 100644 --- a/packages/router-generator/src/filesystem/virtual/getRouteNodes.ts +++ b/packages/router-generator/src/filesystem/virtual/getRouteNodes.ts @@ -1,5 +1,6 @@ import path, { join, resolve } from 'node:path' import { + cleanPath, determineInitialRoutePath, removeExt, removeLeadingSlash, @@ -164,10 +165,14 @@ export async function getRouteNodesRecursive( subtreeNode.variableName = routePathToVariable( `${node.pathPrefix}/${removeExt(subtreeNode.filePath)}`, ) - subtreeNode.routePath = `${parent?.routePath ?? ''}${node.pathPrefix}${subtreeNode.routePath}` + subtreeNode.routePath = cleanPath( + `${parent?.routePath ?? ''}${node.pathPrefix}${subtreeNode.routePath}`, + ) // Keep originalRoutePath aligned with routePath for escape detection if (subtreeNode.originalRoutePath) { - subtreeNode.originalRoutePath = `${parent?.routePath ?? ''}${node.pathPrefix}${subtreeNode.originalRoutePath}` + subtreeNode.originalRoutePath = cleanPath( + `${parent?.routePath ?? ''}${node.pathPrefix}${subtreeNode.originalRoutePath}`, + ) } subtreeNode.filePath = `${node.directory}/${subtreeNode.filePath}` }) diff --git a/packages/router-generator/tests/generator/virtual-inside-nested/routeTree.snapshot.ts b/packages/router-generator/tests/generator/virtual-inside-nested/routeTree.snapshot.ts index 174f18465b8..9f60b82e06d 100644 --- a/packages/router-generator/tests/generator/virtual-inside-nested/routeTree.snapshot.ts +++ b/packages/router-generator/tests/generator/virtual-inside-nested/routeTree.snapshot.ts @@ -11,8 +11,8 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' import { Route as FooBarRouteImport } from './routes/foo/bar' -import { Route as fooBarDetailsRouteImport } from './routes/foo/bar/details' -import { Route as fooBarHomeRouteImport } from './routes/foo/bar/home' +import { Route as FooBarDetailsRouteImport } from './routes/foo/bar/details' +import { Route as FooBarHomeRouteImport } from './routes/foo/bar/home' const IndexRoute = IndexRouteImport.update({ id: '/', @@ -24,12 +24,12 @@ const FooBarRoute = FooBarRouteImport.update({ path: '/foo/bar', getParentRoute: () => rootRouteImport, } as any) -const fooBarDetailsRoute = fooBarDetailsRouteImport.update({ +const FooBarDetailsRoute = FooBarDetailsRouteImport.update({ id: '/$id', path: '/$id', getParentRoute: () => FooBarRoute, } as any) -const fooBarHomeRoute = fooBarHomeRouteImport.update({ +const FooBarHomeRoute = FooBarHomeRouteImport.update({ id: '/', path: '/', getParentRoute: () => FooBarRoute, @@ -38,20 +38,20 @@ const fooBarHomeRoute = fooBarHomeRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/foo/bar': typeof FooBarRouteWithChildren - '/foo/bar/': typeof fooBarHomeRoute - '/foo/bar/$id': typeof fooBarDetailsRoute + '/foo/bar/': typeof FooBarHomeRoute + '/foo/bar/$id': typeof FooBarDetailsRoute } export interface FileRoutesByTo { '/': typeof IndexRoute - '/foo/bar': typeof fooBarHomeRoute - '/foo/bar/$id': typeof fooBarDetailsRoute + '/foo/bar': typeof FooBarHomeRoute + '/foo/bar/$id': typeof FooBarDetailsRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/foo/bar': typeof FooBarRouteWithChildren - '/foo/bar/': typeof fooBarHomeRoute - '/foo/bar/$id': typeof fooBarDetailsRoute + '/foo/bar/': typeof FooBarHomeRoute + '/foo/bar/$id': typeof FooBarDetailsRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -86,27 +86,27 @@ declare module '@tanstack/react-router' { id: '/foo/bar/$id' path: '/$id' fullPath: '/foo/bar/$id' - preLoaderRoute: typeof fooBarDetailsRouteImport + preLoaderRoute: typeof FooBarDetailsRouteImport parentRoute: typeof FooBarRoute } '/foo/bar/': { id: '/foo/bar/' path: '/' fullPath: '/foo/bar/' - preLoaderRoute: typeof fooBarHomeRouteImport + preLoaderRoute: typeof FooBarHomeRouteImport parentRoute: typeof FooBarRoute } } } interface FooBarRouteChildren { - fooBarHomeRoute: typeof fooBarHomeRoute - fooBarDetailsRoute: typeof fooBarDetailsRoute + FooBarHomeRoute: typeof FooBarHomeRoute + FooBarDetailsRoute: typeof FooBarDetailsRoute } const FooBarRouteChildren: FooBarRouteChildren = { - fooBarHomeRoute: fooBarHomeRoute, - fooBarDetailsRoute: fooBarDetailsRoute, + FooBarHomeRoute: FooBarHomeRoute, + FooBarDetailsRoute: FooBarDetailsRoute, } const FooBarRouteWithChildren = diff --git a/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routeTree.snapshot.ts b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routeTree.snapshot.ts index 28aebc0aad0..664cea729c2 100644 --- a/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routeTree.snapshot.ts +++ b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routeTree.snapshot.ts @@ -10,26 +10,26 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' -import { Route as nestedCallbackRouteImport } from './routes/nested/callback' -import { Route as nestedAuthRouteImport } from './routes/nested/auth' -import { Route as nestedHomeRouteImport } from './routes/nested/home' +import { Route as NestedCallbackRouteImport } from './routes/nested/callback' +import { Route as NestedAuthRouteImport } from './routes/nested/auth' +import { Route as NestedHomeRouteImport } from './routes/nested/home' const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, } as any) -const nestedCallbackRoute = nestedCallbackRouteImport.update({ +const NestedCallbackRoute = NestedCallbackRouteImport.update({ id: '/nested/_callback', path: '/nested/_callback', getParentRoute: () => rootRouteImport, } as any) -const nestedAuthRoute = nestedAuthRouteImport.update({ +const NestedAuthRoute = NestedAuthRouteImport.update({ id: '/nested/_auth', path: '/nested/_auth', getParentRoute: () => rootRouteImport, } as any) -const nestedHomeRoute = nestedHomeRouteImport.update({ +const NestedHomeRoute = NestedHomeRouteImport.update({ id: '/nested/', path: '/nested/', getParentRoute: () => rootRouteImport, @@ -37,22 +37,22 @@ const nestedHomeRoute = nestedHomeRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute - '/nested/': typeof nestedHomeRoute - '/nested/_auth': typeof nestedAuthRoute - '/nested/_callback': typeof nestedCallbackRoute + '/nested/': typeof NestedHomeRoute + '/nested/_auth': typeof NestedAuthRoute + '/nested/_callback': typeof NestedCallbackRoute } export interface FileRoutesByTo { '/': typeof IndexRoute - '/nested': typeof nestedHomeRoute - '/nested/_auth': typeof nestedAuthRoute - '/nested/_callback': typeof nestedCallbackRoute + '/nested': typeof NestedHomeRoute + '/nested/_auth': typeof NestedAuthRoute + '/nested/_callback': typeof NestedCallbackRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute - '/nested/': typeof nestedHomeRoute - '/nested/_auth': typeof nestedAuthRoute - '/nested/_callback': typeof nestedCallbackRoute + '/nested/': typeof NestedHomeRoute + '/nested/_auth': typeof NestedAuthRoute + '/nested/_callback': typeof NestedCallbackRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -64,9 +64,9 @@ export interface FileRouteTypes { } export interface RootRouteChildren { IndexRoute: typeof IndexRoute - nestedHomeRoute: typeof nestedHomeRoute - nestedAuthRoute: typeof nestedAuthRoute - nestedCallbackRoute: typeof nestedCallbackRoute + NestedHomeRoute: typeof NestedHomeRoute + NestedAuthRoute: typeof NestedAuthRoute + NestedCallbackRoute: typeof NestedCallbackRoute } declare module '@tanstack/react-router' { @@ -82,21 +82,21 @@ declare module '@tanstack/react-router' { id: '/nested/_callback' path: '/nested/_callback' fullPath: '/nested/_callback' - preLoaderRoute: typeof nestedCallbackRouteImport + preLoaderRoute: typeof NestedCallbackRouteImport parentRoute: typeof rootRouteImport } '/nested/_auth': { id: '/nested/_auth' path: '/nested/_auth' fullPath: '/nested/_auth' - preLoaderRoute: typeof nestedAuthRouteImport + preLoaderRoute: typeof NestedAuthRouteImport parentRoute: typeof rootRouteImport } '/nested/': { id: '/nested/' path: '/nested' fullPath: '/nested/' - preLoaderRoute: typeof nestedHomeRouteImport + preLoaderRoute: typeof NestedHomeRouteImport parentRoute: typeof rootRouteImport } } @@ -104,9 +104,9 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, - nestedHomeRoute: nestedHomeRoute, - nestedAuthRoute: nestedAuthRoute, - nestedCallbackRoute: nestedCallbackRoute, + NestedHomeRoute: NestedHomeRoute, + NestedAuthRoute: NestedAuthRoute, + NestedCallbackRoute: NestedCallbackRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/packages/router-generator/tests/generator/virtual-physical-inside-nested/routeTree.snapshot.ts b/packages/router-generator/tests/generator/virtual-physical-inside-nested/routeTree.snapshot.ts new file mode 100644 index 00000000000..94d84fb7904 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-physical-inside-nested/routeTree.snapshot.ts @@ -0,0 +1,134 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as indexRouteImport } from './routes/index' +import { Route as S1LayoutRouteImport } from './subtree/routes/_layout' +import { Route as S1DotDotFooRoutesRouteRouteImport } from './subtree/foo/routes/route' +import { Route as S1DotDotFooRoutesIndexRouteImport } from './subtree/foo/routes/index' + +const indexRoute = indexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const S1LayoutRoute = S1LayoutRouteImport.update({ + id: '/s1/_layout', + path: '/s1', + getParentRoute: () => rootRouteImport, +} as any) +const S1DotDotFooRoutesRouteRoute = S1DotDotFooRoutesRouteRouteImport.update({ + id: '/foo', + path: '/foo', + getParentRoute: () => S1LayoutRoute, +} as any) +const S1DotDotFooRoutesIndexRoute = S1DotDotFooRoutesIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => S1DotDotFooRoutesRouteRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof indexRoute + '/s1': typeof S1LayoutRouteWithChildren + '/s1/foo': typeof S1DotDotFooRoutesRouteRouteWithChildren + '/s1/foo/': typeof S1DotDotFooRoutesIndexRoute +} +export interface FileRoutesByTo { + '/': typeof indexRoute + '/s1': typeof S1LayoutRouteWithChildren + '/s1/foo': typeof S1DotDotFooRoutesIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof indexRoute + '/s1/_layout': typeof S1LayoutRouteWithChildren + '/s1/_layout/foo': typeof S1DotDotFooRoutesRouteRouteWithChildren + '/s1/_layout/foo/': typeof S1DotDotFooRoutesIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/s1' | '/s1/foo' | '/s1/foo/' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/s1' | '/s1/foo' + id: '__root__' | '/' | '/s1/_layout' | '/s1/_layout/foo' | '/s1/_layout/foo/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + indexRoute: typeof indexRoute + S1LayoutRoute: typeof S1LayoutRouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof indexRouteImport + parentRoute: typeof rootRouteImport + } + '/s1/_layout': { + id: '/s1/_layout' + path: '/s1' + fullPath: '/s1' + preLoaderRoute: typeof S1LayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/s1/_layout/foo': { + id: '/s1/_layout/foo' + path: '/foo' + fullPath: '/s1/foo' + preLoaderRoute: typeof S1DotDotFooRoutesRouteRouteImport + parentRoute: typeof S1LayoutRoute + } + '/s1/_layout/foo/': { + id: '/s1/_layout/foo/' + path: '/' + fullPath: '/s1/foo/' + preLoaderRoute: typeof S1DotDotFooRoutesIndexRouteImport + parentRoute: typeof S1DotDotFooRoutesRouteRoute + } + } +} + +interface S1DotDotFooRoutesRouteRouteChildren { + S1DotDotFooRoutesIndexRoute: typeof S1DotDotFooRoutesIndexRoute +} + +const S1DotDotFooRoutesRouteRouteChildren: S1DotDotFooRoutesRouteRouteChildren = + { + S1DotDotFooRoutesIndexRoute: S1DotDotFooRoutesIndexRoute, + } + +const S1DotDotFooRoutesRouteRouteWithChildren = + S1DotDotFooRoutesRouteRoute._addFileChildren( + S1DotDotFooRoutesRouteRouteChildren, + ) + +interface S1LayoutRouteChildren { + S1DotDotFooRoutesRouteRoute: typeof S1DotDotFooRoutesRouteRouteWithChildren +} + +const S1LayoutRouteChildren: S1LayoutRouteChildren = { + S1DotDotFooRoutesRouteRoute: S1DotDotFooRoutesRouteRouteWithChildren, +} + +const S1LayoutRouteWithChildren = S1LayoutRoute._addFileChildren( + S1LayoutRouteChildren, +) + +const rootRouteChildren: RootRouteChildren = { + indexRoute: indexRoute, + S1LayoutRoute: S1LayoutRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/packages/router-generator/tests/generator/virtual-physical-inside-nested/routes.ts b/packages/router-generator/tests/generator/virtual-physical-inside-nested/routes.ts new file mode 100644 index 00000000000..c12b588011f --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-physical-inside-nested/routes.ts @@ -0,0 +1,6 @@ +import { index, physical, rootRoute } from '@tanstack/virtual-file-routes' + +export const routes = rootRoute('__root.tsx', [ + index('index.tsx'), + physical('/s1', '../subtree/routes'), +]) diff --git a/packages/router-generator/tests/generator/virtual-physical-inside-nested/routes/__root.tsx b/packages/router-generator/tests/generator/virtual-physical-inside-nested/routes/__root.tsx new file mode 100644 index 00000000000..f463b796b44 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-physical-inside-nested/routes/__root.tsx @@ -0,0 +1,5 @@ +import { createRootRoute, Outlet } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: () => , +}) diff --git a/packages/router-generator/tests/generator/virtual-physical-inside-nested/routes/index.tsx b/packages/router-generator/tests/generator/virtual-physical-inside-nested/routes/index.tsx new file mode 100644 index 00000000000..fa6ed849667 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-physical-inside-nested/routes/index.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: () =>
Home
, +}) diff --git a/packages/router-generator/tests/generator/virtual-physical-inside-nested/subtree/foo/routes/index.tsx b/packages/router-generator/tests/generator/virtual-physical-inside-nested/subtree/foo/routes/index.tsx new file mode 100644 index 00000000000..0afeadc25e7 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-physical-inside-nested/subtree/foo/routes/index.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/s1/_layout/foo/')({ + component: () =>
Nested home
, +}) diff --git a/packages/router-generator/tests/generator/virtual-physical-inside-nested/subtree/foo/routes/route.tsx b/packages/router-generator/tests/generator/virtual-physical-inside-nested/subtree/foo/routes/route.tsx new file mode 100644 index 00000000000..31746ba2589 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-physical-inside-nested/subtree/foo/routes/route.tsx @@ -0,0 +1,5 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/s1/_layout/foo')({ + component: () => , +}) diff --git a/packages/router-generator/tests/generator/virtual-physical-inside-nested/subtree/routes/__virtual.ts b/packages/router-generator/tests/generator/virtual-physical-inside-nested/subtree/routes/__virtual.ts new file mode 100644 index 00000000000..28ca02884c9 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-physical-inside-nested/subtree/routes/__virtual.ts @@ -0,0 +1,9 @@ +import { + defineVirtualSubtreeConfig, + layout, + physical, +} from '@tanstack/virtual-file-routes' + +export default defineVirtualSubtreeConfig([ + layout('./_layout.tsx', [physical('/foo', '../foo/routes')]), +]) diff --git a/packages/router-generator/tests/generator/virtual-physical-inside-nested/subtree/routes/_layout.tsx b/packages/router-generator/tests/generator/virtual-physical-inside-nested/subtree/routes/_layout.tsx new file mode 100644 index 00000000000..4740cb1263a --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-physical-inside-nested/subtree/routes/_layout.tsx @@ -0,0 +1,5 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/s1/_layout')({ + component: () => , +}) diff --git a/packages/router-generator/tests/generator/virtual-physical-inside-nested/tsr.config.json b/packages/router-generator/tests/generator/virtual-physical-inside-nested/tsr.config.json new file mode 100644 index 00000000000..4d587108e38 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-physical-inside-nested/tsr.config.json @@ -0,0 +1,5 @@ +{ + "routesDirectory": "./routes", + "generatedRouteTree": "./routeTree.gen.ts", + "virtualRouteConfig": "./routes.ts" +}