diff --git a/crates/tuono/Cargo.toml b/crates/tuono/Cargo.toml index aab19b55..8c288857 100644 --- a/crates/tuono/Cargo.toml +++ b/crates/tuono/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono" -version = "0.7.0" +version = "0.8.0" edition = "2021" authors = ["V. Ageno "] description = "The react/rust fullstack framework" diff --git a/crates/tuono_lib/Cargo.toml b/crates/tuono_lib/Cargo.toml index 10277b93..8bfe8217 100644 --- a/crates/tuono_lib/Cargo.toml +++ b/crates/tuono_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono_lib" -version = "0.7.0" +version = "0.8.0" edition = "2021" authors = ["V. Ageno "] description = "The react/rust fullstack framework" @@ -32,7 +32,7 @@ regex = "1.10.5" either = "1.13.0" tower-http = {version = "0.5.2", features = ["fs"]} -tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.7.0"} +tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.8.0"} # Match the same version used by axum tokio-tungstenite = "0.21.0" futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } diff --git a/crates/tuono_lib_macros/Cargo.toml b/crates/tuono_lib_macros/Cargo.toml index 6229b5ed..89733373 100644 --- a/crates/tuono_lib_macros/Cargo.toml +++ b/crates/tuono_lib_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono_lib_macros" -version = "0.7.0" +version = "0.8.0" edition = "2021" description = "The react/rust fullstack framework" keywords = [ "react", "typescript", "fullstack", "web", "ssr"] diff --git a/packages/fs-router-vite-plugin/package.json b/packages/fs-router-vite-plugin/package.json index 65713c8d..d33e6f7c 100644 --- a/packages/fs-router-vite-plugin/package.json +++ b/packages/fs-router-vite-plugin/package.json @@ -1,6 +1,6 @@ { "name": "tuono-fs-router-vite-plugin", - "version": "0.7.0", + "version": "0.8.0", "description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework", "scripts": { "dev": "vite build --watch", diff --git a/packages/fs-router-vite-plugin/src/build-route-config.spec.ts b/packages/fs-router-vite-plugin/src/build-route-config.spec.ts new file mode 100644 index 00000000..327fb421 --- /dev/null +++ b/packages/fs-router-vite-plugin/src/build-route-config.spec.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from 'vitest' +import { buildRouteConfig } from './build-route-config' + +const routes = [ + { + filePath: 'posts/my-post.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/my-post.tsx', + routePath: '/posts/my-post', + variableName: 'PostsMyPost', + parent: { + filePath: 'posts/__root.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/__root.tsx', + routePath: '/posts/__root', + variableName: 'Postsroot', + path: '/posts/__root', + cleanedPath: '/posts', + children: undefined, + }, + path: '/posts/my-post', + cleanedPath: '/posts/my-post', + }, + { + filePath: 'posts/index.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/index.tsx', + routePath: '/posts/', + variableName: 'PostsIndex', + parent: { + filePath: 'posts/__root.tsx', + fullPath: + '/home/valerio/Documents/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/__root.tsx', + routePath: '/posts/__root', + variableName: 'Postsroot', + path: '/posts/__root', + cleanedPath: '/posts', + children: undefined, + }, + path: '/posts/', + cleanedPath: '/posts/', + }, + { + filePath: 'posts/[post].tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/index.tsx', + routePath: '/posts/', + variableName: 'PostspostIndex', + parent: { + filePath: 'posts/__root.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/__root.tsx', + routePath: '/posts/__root', + variableName: 'Postsroot', + path: '/posts/__root', + cleanedPath: '/posts', + children: undefined, + }, + path: '/posts/', + cleanedPath: '/posts/', + }, +] + +describe('buildRouteConfig works', async () => { + it('Should build the correct config', () => { + const expectedConfig = + 'PostsMyPostRoute,PostsIndexRoute,PostspostIndexRoute' + const config = buildRouteConfig(routes) + expect(config).toStrictEqual(expectedConfig) + }) +}) diff --git a/packages/fs-router-vite-plugin/src/build-route-config.ts b/packages/fs-router-vite-plugin/src/build-route-config.ts new file mode 100644 index 00000000..1ab66c1e --- /dev/null +++ b/packages/fs-router-vite-plugin/src/build-route-config.ts @@ -0,0 +1,17 @@ +import { spaces } from './utils' +import type { RouteNode } from './types' + +export function buildRouteConfig(nodes: RouteNode[], depth = 1): string { + const children = nodes.map((node) => { + const route = `${node.variableName}Route` + + if (node.children?.length) { + const childConfigs = buildRouteConfig(node.children, depth + 1) + return `${route}.addChildren([${spaces(depth * 4)}${childConfigs}])` + } + + return route + }) + + return children.filter(Boolean).join(`,`) +} diff --git a/packages/fs-router-vite-plugin/src/generator.ts b/packages/fs-router-vite-plugin/src/generator.ts index a61ad2e5..dcb52460 100644 --- a/packages/fs-router-vite-plugin/src/generator.ts +++ b/packages/fs-router-vite-plugin/src/generator.ts @@ -1,15 +1,17 @@ import * as fsp from 'fs/promises' import path from 'path' +import { buildRouteConfig } from './build-route-config' +import { hasParentRoute } from './has-parent-route' import { cleanPath, determineNodePath, multiSortBy, - spaces, replaceBackslash, removeExt, routePathToVariable, removeGroups, + removeLastSlash, removeUnderscores, removeLayoutSegments, } from './utils' @@ -18,6 +20,7 @@ import type { Config, RouteNode } from './types' import { ROUTES_FOLDER, ROOT_PATH_ID, GENERATED_ROUTE_TREE } from './constants' import { format } from 'prettier' +import { sortRouteNodes } from './sort-route-nodes' let latestTask = 0 @@ -56,9 +59,6 @@ async function getRouteNodes( // Remove the index from the route path and // if the route path is empty, use `/' - - const isLoader = routePath.includes('-loading') - if (routePath === 'index') { routePath = '/' } @@ -69,7 +69,6 @@ async function getRouteNodes( filePath, fullPath, routePath, - isLoader, variableName, }) } else if (fullPath.match(/\.(rs)$/)) { @@ -95,38 +94,6 @@ async function getRouteNodes( return { routeNodes, rustHandlersNodes } } -export function hasParentRoute( - routes: RouteNode[], - node: RouteNode, - routePathToCheck: string | undefined, -): RouteNode | null { - if (!routePathToCheck || routePathToCheck === '/') { - return null - } - - const sortedNodes = multiSortBy(routes, [ - (d): number => d.routePath.length * -1, - (d): string | undefined => d.variableName, - ]).filter((d) => d.routePath !== `/${ROOT_PATH_ID}`) - - for (const route of sortedNodes) { - if (route.routePath === '/') continue - - if ( - routePathToCheck.startsWith(`${route.routePath}/`) && - route.routePath !== routePathToCheck - ) { - return route - } - } - - const segments = routePathToCheck.split('/') - segments.pop() // Remove the last segment - const parentRoutePath = segments.join('/') - - return hasParentRoute(routes, node, parentRoutePath) -} - export async function routeGenerator(config = defaultConfig): Promise { if (!isFirst) { isFirst = true @@ -149,25 +116,12 @@ export async function routeGenerator(config = defaultConfig): Promise { const { routeNodes: beforeRouteNodes, rustHandlersNodes } = await getRouteNodes(config) - const preRouteNodes = multiSortBy(beforeRouteNodes, [ - (d): number => (d.routePath === '/' ? -1 : 1), - (d): number => d.routePath.split('/').length, - (d): number => (d.filePath.match(/[./]index[.]/) ? 1 : -1), - (d): number => - d.filePath.match( - /[./](component|errorComponent|pendingComponent|loader|lazy)[.]/, - ) - ? 1 - : -1, - (d): number => (d.filePath.match(/[./]route[.]/) ? -1 : 1), - (d): number => (d.routePath.endsWith('/') ? -1 : 1), - (d): string => d.routePath, - ]).filter((d) => ![`/${ROOT_PATH_ID}`].includes(d.routePath || '')) + const preRouteNodes = sortRouteNodes(beforeRouteNodes) + + const routeNodes: RouteNode[] = [] // Loop over the flat list of routeNodes and // build up a tree based on the routeNodes' routePath - const routeNodes: RouteNode[] = [] - const handleNode = async (node: RouteNode): Promise => { const parentRoute = hasParentRoute(routeNodes, node, node.routePath) @@ -175,8 +129,8 @@ export async function routeGenerator(config = defaultConfig): Promise { node.path = determineNodePath(node) - node.cleanedPath = removeGroups( - removeUnderscores(removeLayoutSegments(node.path)) ?? '', + node.cleanedPath = removeLastSlash( + removeGroups(removeUnderscores(removeLayoutSegments(node.path)) ?? ''), ) if (node.parent) { @@ -190,25 +144,6 @@ export async function routeGenerator(config = defaultConfig): Promise { await handleNode(node) } - function buildRouteConfig(nodes: RouteNode[], depth = 1): string { - const children = nodes.map((node) => { - if (node.isLayout) { - return - } - - const route = `${node.variableName}Route` - - if (node.children?.length) { - const childConfigs = buildRouteConfig(node.children, depth + 1) - return `${route}.addChildren([${spaces(depth * 4)}${childConfigs}])` - } - - return route - }) - - return children.filter(Boolean).join(`,`) - } - const routeConfigChildrenText = buildRouteConfig(routeNodes) const sortedRouteNodes = multiSortBy(routeNodes, [ @@ -237,7 +172,10 @@ export async function routeGenerator(config = defaultConfig): Promise { const createRoutes = [ ...sortedRouteNodes.map((node) => { - return `const ${node.variableName} = createRoute({ component: ${node.variableName}Import })` + const isRoot = node.routePath.endsWith(ROOT_PATH_ID) + const rootDeclaration = isRoot ? ', isRoot: true' : '' + + return `const ${node.variableName} = createRoute({ component: ${node.variableName}Import${rootDeclaration} })` }), ].join('\n') @@ -247,7 +185,7 @@ export async function routeGenerator(config = defaultConfig): Promise { return [ `const ${node.variableName}Route = ${node.variableName}.update({ ${[ - `path: '${node.cleanedPath}'`, + !node.path?.endsWith(ROOT_PATH_ID) && `path: '${node.cleanedPath}'`, `getParentRoute: () => ${node.parent?.variableName ?? 'root'}Route`, rustHandlersNodes.includes(node.path || '') ? 'hasHandler: true' diff --git a/packages/fs-router-vite-plugin/src/has-parent-route.spec.ts b/packages/fs-router-vite-plugin/src/has-parent-route.spec.ts new file mode 100644 index 00000000..615a9f30 --- /dev/null +++ b/packages/fs-router-vite-plugin/src/has-parent-route.spec.ts @@ -0,0 +1,83 @@ +import { describe, it, expect } from 'vitest' +import { hasParentRoute } from './has-parent-route' + +const routes = [ + { + filePath: 'posts/[post].tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root/routes/posts/[post].tsx', + routePath: '/posts/[post]', + variableName: 'Postspost', + path: '/posts/[post]', + cleanedPath: '/posts/[post]', + }, + { + filePath: 'posts/__root.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root/routes/posts/__root.tsx', + routePath: '/posts/__root', + variableName: 'Postsroot', + path: '/posts/__root', + cleanedPath: '/posts', + }, + { + filePath: 'index.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root/routes/index.tsx', + routePath: '/', + variableName: 'Index', + path: '/', + cleanedPath: '/', + }, + { + filePath: 'posts/my-post.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root/routes/posts/my-post.tsx', + routePath: '/posts/my-post', + variableName: 'PostsMyPost', + path: '/posts/my-post', + cleanedPath: '/posts/my-post', + }, +] + +const parent = { + filePath: 'posts/__root.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root/routes/posts/__root.tsx', + routePath: '/posts/__root', + variableName: 'Postsroot', + path: '/posts/__root', + cleanedPath: '/posts', +} + +const myPost = { + filePath: 'posts/my-post.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root/routes/posts/my-post.tsx', + routePath: '/posts/my-post', + variableName: 'PostsMyPost', + path: '/posts/my-post', + cleanedPath: '/posts/my-post', +} + +const dynamicRoute = { + filePath: 'posts/[post].tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root/routes/posts/[post].tsx', + routePath: '/posts/[post]', + variableName: 'Postspost', + path: '/posts/[post]', + cleanedPath: '/posts/[post]', +} + +describe('hasParentRoute works', async () => { + it('Should detect parent route', () => { + const parentRoute = hasParentRoute(routes, myPost, myPost.path) + expect(parentRoute).toStrictEqual(parent) + }) + + it('Should detect parent route for dynamic routes', () => { + const parentRoute = hasParentRoute(routes, dynamicRoute, dynamicRoute.path) + expect(parentRoute).toStrictEqual(parent) + }) +}) diff --git a/packages/fs-router-vite-plugin/src/has-parent-route.ts b/packages/fs-router-vite-plugin/src/has-parent-route.ts new file mode 100644 index 00000000..12b137c5 --- /dev/null +++ b/packages/fs-router-vite-plugin/src/has-parent-route.ts @@ -0,0 +1,37 @@ +import { ROOT_PATH_ID } from './constants' +import { multiSortBy } from './utils' +import type { RouteNode } from './types' + +export function hasParentRoute( + routes: RouteNode[], + node: RouteNode, + routePathToCheck = '/', +): RouteNode | null { + const segments = routePathToCheck.split('/') + segments.pop() // Remove the last segment + const parentRoutePath = segments.join('/') + + if (!parentRoutePath || parentRoutePath === '/') { + return null + } + + const sortedNodes = multiSortBy(routes, [ + (d): number => d.routePath.length * -1, + (d): string | undefined => d.variableName, + ]) + // Exclude base __root file + .filter((d) => d.routePath !== `/${ROOT_PATH_ID}`) + + for (const route of sortedNodes) { + if (route.routePath === '/') continue + + if ( + route.routePath.startsWith(parentRoutePath) && + route.routePath.endsWith(ROOT_PATH_ID) + ) { + return route + } + } + + return hasParentRoute(routes, node, parentRoutePath) +} diff --git a/packages/fs-router-vite-plugin/src/sort-route-nodes.spec.ts b/packages/fs-router-vite-plugin/src/sort-route-nodes.spec.ts new file mode 100644 index 00000000..6a0ccb69 --- /dev/null +++ b/packages/fs-router-vite-plugin/src/sort-route-nodes.spec.ts @@ -0,0 +1,114 @@ +import { describe, it, expect } from 'vitest' +import { sortRouteNodes } from './sort-route-nodes' + +const routes = [ + { + filePath: 'index.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/index.tsx', + routePath: '/', + variableName: 'Index', + path: '/', + cleanedPath: '/', + }, + { + filePath: 'about.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/about.tsx', + routePath: '/about', + variableName: 'About', + path: '/about', + cleanedPath: '/about', + }, + { + filePath: '__root.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/__root.tsx', + routePath: '/__root', + variableName: 'root', + }, + { + filePath: 'posts/[post].tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/[post].tsx', + routePath: '/posts/[post]', + variableName: 'Postspost', + }, + { + filePath: 'posts/my-post.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/my-post.tsx', + routePath: '/posts/my-post', + variableName: 'PostsMyPost', + }, + { + filePath: 'posts/index.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/index.tsx', + routePath: '/posts/', + variableName: 'PostsIndex', + }, + { + filePath: 'posts/__root.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/__root.tsx', + routePath: '/posts/__root', + variableName: 'Postsroot', + }, +] + +const expectedSorting = [ + { + filePath: 'index.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/index.tsx', + routePath: '/', + variableName: 'Index', + path: '/', + cleanedPath: '/', + }, + { + filePath: 'about.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/about.tsx', + routePath: '/about', + variableName: 'About', + path: '/about', + cleanedPath: '/about', + }, + { + filePath: 'posts/__root.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/__root.tsx', + routePath: '/posts/__root', + variableName: 'Postsroot', + }, + { + filePath: 'posts/my-post.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/my-post.tsx', + routePath: '/posts/my-post', + variableName: 'PostsMyPost', + }, + { + filePath: 'posts/index.tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/index.tsx', + routePath: '/posts/', + variableName: 'PostsIndex', + }, + { + filePath: 'posts/[post].tsx', + fullPath: + '/tuono/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/[post].tsx', + routePath: '/posts/[post]', + variableName: 'Postspost', + }, +] + +describe('sortRouteNodes works', async () => { + it('Should correctly sort the nodes', () => { + const sorted = sortRouteNodes(routes) + expect(sorted).toStrictEqual(expectedSorting) + }) +}) diff --git a/packages/fs-router-vite-plugin/src/sort-route-nodes.ts b/packages/fs-router-vite-plugin/src/sort-route-nodes.ts new file mode 100644 index 00000000..9273f997 --- /dev/null +++ b/packages/fs-router-vite-plugin/src/sort-route-nodes.ts @@ -0,0 +1,17 @@ +import type { RouteNode } from './types' +import { multiSortBy } from './utils' +import { ROOT_PATH_ID } from './constants' + +// Routes need to be sorted in order to iterate over the handleNode fn +// with first the items that might be parent routes +export const sortRouteNodes = (routes: RouteNode[]): RouteNode[] => + multiSortBy(routes, [ + (d): number => (d.routePath === '/' ? -1 : 1), + (d): number => d.routePath.split('/').length, + // Dynamic route + (d): number => (d.routePath.endsWith(']') ? 1 : -1), + (d): number => (d.filePath.match(/[./]index[.]/) ? 1 : -1), + (d): number => (d.filePath.match(/[./]route[.]/) ? -1 : 1), + (d): number => (d.routePath.endsWith('/') ? -1 : 1), + (d): string => d.routePath, + ]).filter((d) => ![`/${ROOT_PATH_ID}`].includes(d.routePath || '')) diff --git a/packages/fs-router-vite-plugin/src/types.ts b/packages/fs-router-vite-plugin/src/types.ts index 56fe3cbe..10ade7a2 100644 --- a/packages/fs-router-vite-plugin/src/types.ts +++ b/packages/fs-router-vite-plugin/src/types.ts @@ -5,7 +5,6 @@ export interface RouteNode { path?: string cleanedPath?: string isLayout?: boolean - isLoader: boolean children?: RouteNode[] parent?: RouteNode variableName?: string diff --git a/packages/fs-router-vite-plugin/src/utils.ts b/packages/fs-router-vite-plugin/src/utils.ts index 35b35ee5..b21e3912 100644 --- a/packages/fs-router-vite-plugin/src/utils.ts +++ b/packages/fs-router-vite-plugin/src/utils.ts @@ -100,6 +100,12 @@ export function trimPathLeft(pathToTrim: string): string { return pathToTrim === '/' ? pathToTrim : pathToTrim.replace(/^\/{1,}/, '') } +export function removeLastSlash(str: string): string { + if (str.length > 1 && str.endsWith('/')) + return str.substring(0, str.length - 1) + return str +} + /** * The `node.path` is used as the `id` in the route definition. * This function checks if the given node has a parent and if so, it determines the correct path for the given node. diff --git a/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routeTree.expected.ts b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routeTree.expected.ts new file mode 100644 index 00000000..b1211525 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routeTree.expected.ts @@ -0,0 +1,67 @@ +// This file is auto-generated by Tuono + +import { createRoute, dynamic } from 'tuono' + +import RootImport from './routes/__root' + +const PostsrootImport = dynamic(() => import('./routes/posts/__root')) +const AboutImport = dynamic(() => import('./routes/about')) +const IndexImport = dynamic(() => import('./routes/index')) +const PostspostImport = dynamic(() => import('./routes/posts/[post]')) +const PostsIndexImport = dynamic(() => import('./routes/posts/index')) +const PostsMyPostImport = dynamic(() => import('./routes/posts/my-post')) + +const rootRoute = createRoute({ isRoot: true, component: RootImport }) + +const Postsroot = createRoute({ component: PostsrootImport, isRoot: true }) +const About = createRoute({ component: AboutImport }) +const Index = createRoute({ component: IndexImport }) +const Postspost = createRoute({ component: PostspostImport }) +const PostsIndex = createRoute({ component: PostsIndexImport }) +const PostsMyPost = createRoute({ component: PostsMyPostImport }) + +// Create/Update Routes + +const PostsrootRoute = Postsroot.update({ + getParentRoute: () => rootRoute, +}) + +const AboutRoute = About.update({ + path: '/about', + getParentRoute: () => rootRoute, +}) + +const IndexRoute = Index.update({ + path: '/', + getParentRoute: () => rootRoute, +}) + +const PostspostRoute = Postspost.update({ + path: '/posts/[post]', + getParentRoute: () => PostsrootRoute, +}) + +const PostsIndexRoute = PostsIndex.update({ + path: '/posts', + getParentRoute: () => PostsrootRoute, +}) + +const PostsMyPostRoute = PostsMyPost.update({ + path: '/posts/my-post', + getParentRoute: () => PostsrootRoute, +}) + +// Create and export the route tree + +export const routeTree = rootRoute.addChildren([ + IndexRoute, + AboutRoute, + PostsrootRoute.addChildren([ + PostsMyPostRoute, + PostsIndexRoute, + PostspostRoute, + ]), + PostsMyPostRoute, + PostsIndexRoute, + PostspostRoute, +]) diff --git a/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/__root.tsx b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/__root.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/__root.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/about.tsx b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/about.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/about.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/index.tsx b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/index.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/index.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/[post].tsx b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/[post].tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/[post].tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/__root.tsx b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/__root.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/__root.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/index.tsx b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/index.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/index.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/my-post.tsx b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/my-post.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/my-post.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/lazy-fn-vite-plugin/package.json b/packages/lazy-fn-vite-plugin/package.json index ab067ad2..182d25c5 100644 --- a/packages/lazy-fn-vite-plugin/package.json +++ b/packages/lazy-fn-vite-plugin/package.json @@ -1,6 +1,6 @@ { "name": "tuono-lazy-fn-vite-plugin", - "version": "0.7.0", + "version": "0.8.0", "description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework", "scripts": { "dev": "vite build --watch", diff --git a/packages/tuono/package.json b/packages/tuono/package.json index bd5f0e33..7002e22a 100644 --- a/packages/tuono/package.json +++ b/packages/tuono/package.json @@ -1,6 +1,6 @@ { "name": "tuono", - "version": "0.7.0", + "version": "0.8.0", "description": "The react/rust fullstack framework", "scripts": { "dev": "vite build --watch", diff --git a/packages/tuono/src/router/components/RouteMatch.spec.tsx b/packages/tuono/src/router/components/RouteMatch.spec.tsx new file mode 100644 index 00000000..9de7a97d --- /dev/null +++ b/packages/tuono/src/router/components/RouteMatch.spec.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import { afterEach, describe, expect, test, vi } from 'vitest' +import { RouteMatch } from './RouteMatch' +import { cleanup, render, screen } from '@testing-library/react' +import type { Route } from '../route' +import '@testing-library/jest-dom' + +interface Props { + children: React.ReactNode +} + +const root = { + isRoot: true, + component: ({ children }: Props) => ( +
root route {children}
+ ), +} as Route + +const parent = { + component: ({ children }: Props) => ( +
parent route {children}
+ ), + options: { + getParentRoute: () => root, + }, +} as Route + +const route = { + component: () =>

current route

, + options: { + getParentRoute: () => parent, + }, +} as Route + +describe('Test RouteMatch component', () => { + afterEach(() => { + cleanup() + }) + + test('It should correctly render nested routes', () => { + vi.mock('../hooks/useServerSideProps.tsx', () => ({ + useServerSideProps: (): { data: any; isLoading: boolean } => { + return { + data: undefined, + isLoading: false, + } + }, + })) + + render() + expect(screen.getByTestId('root')).toHaveTextContent( + 'root route parent route current route', + ) + expect(screen.getByTestId('route')).toHaveTextContent('current route') + }) +}) diff --git a/packages/tuono/src/router/components/RouteMatch.tsx b/packages/tuono/src/router/components/RouteMatch.tsx index 6a78147f..5545c68e 100644 --- a/packages/tuono/src/router/components/RouteMatch.tsx +++ b/packages/tuono/src/router/components/RouteMatch.tsx @@ -19,20 +19,63 @@ export const RouteMatch = ({ }: MatchProps): JSX.Element => { const { data, isLoading } = useServerSideProps(route, serverSideProps) - if (!route.isRoot) { - const Root = route.options.getParentRoute() - return ( - - - - - - ) - } + const routes = React.useMemo(() => { + const components = loadParentComponents(route) + components.push(route) + return components + }, [route.id]) return ( - - - + ) } + +interface TraverseRootComponentsProps { + routes: Route[] + data: any + isLoading: boolean + children?: React.ReactNode + index?: number +} + +interface ParentProps { + children: React.ReactNode + data: any + isLoading: boolean +} + +const TraverseRootComponents = ({ + routes, + data, + isLoading, + index = 0, +}: TraverseRootComponentsProps): JSX.Element => { + const Parent = React.memo( + routes[index]?.component as unknown as (props: ParentProps) => JSX.Element, + ) + + return ( + + {Boolean(routes.length > index) && ( + + )} + + ) +} + +const loadParentComponents = (route: Route, loader: Route[] = []): Route[] => { + const parentComponent = route.options?.getParentRoute?.() + + loader.push(parentComponent) + + if (!parentComponent.isRoot) { + return loadParentComponents(parentComponent, loader) + } + + return loader.reverse() +} diff --git a/packages/tuono/src/router/route.ts b/packages/tuono/src/router/route.ts index 6e2a895f..00559cf3 100644 --- a/packages/tuono/src/router/route.ts +++ b/packages/tuono/src/router/route.ts @@ -56,12 +56,7 @@ export class Route { const customId = this.options?.id || path // Strip the parentId prefix from the first level of children - let id = isRoot - ? rootRouteId - : joinPaths([ - this.parentRoute.id === rootRouteId ? '' : this.parentRoute.id, - customId, - ]) + let id = isRoot ? rootRouteId : joinPaths([customId]) if (path === rootRouteId) { path = '/' @@ -71,12 +66,11 @@ export class Route { id = joinPaths(['/', id]) } - const fullPath = - id === rootRouteId ? '/' : joinPaths([this.parentRoute.fullPath, path]) + const fullPath = id === rootRouteId ? '/' : path this.path = path this.id = id - this.fullPath = fullPath + this.fullPath = fullPath || '' } addChildren(routes: Route[]): Route { @@ -91,6 +85,7 @@ export class Route { } } +// TODO: not use yet. To be updated in tuono-fs-router-vite-plugin package export function createRootRoute(options: RouteOptions): Route { return new Route({ ...options, isRoot: true }) }