mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
Support nested roots (#18)
* feat: remove isLoader route property * fix: has-parent-route fs-router util * feat: handle routeGenerator nested __root * feat: enable multiple roots with nested dynamic paths * feat: enable nested routes * fix: prevent route path creation for __root * feat: prevent wrong path on subfolder index routes * chore: cleaned tutorial example folder * feat: update version to v0.8.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_lib"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
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"] }
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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(`,`)
|
||||
}
|
||||
@@ -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<void> {
|
||||
if (!isFirst) {
|
||||
isFirst = true
|
||||
@@ -149,25 +116,12 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
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<void> => {
|
||||
const parentRoute = hasParentRoute(routeNodes, node, node.routePath)
|
||||
|
||||
@@ -175,8 +129,8 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
|
||||
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<void> {
|
||||
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<void> {
|
||||
|
||||
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<void> {
|
||||
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'
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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 || ''))
|
||||
@@ -5,7 +5,6 @@ export interface RouteNode {
|
||||
path?: string
|
||||
cleanedPath?: string
|
||||
isLayout?: boolean
|
||||
isLoader: boolean
|
||||
children?: RouteNode[]
|
||||
parent?: RouteNode
|
||||
variableName?: string
|
||||
|
||||
@@ -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.
|
||||
|
||||
+67
@@ -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,
|
||||
])
|
||||
+1
@@ -0,0 +1 @@
|
||||
/** */
|
||||
+1
@@ -0,0 +1 @@
|
||||
/** */
|
||||
+1
@@ -0,0 +1 @@
|
||||
/** */
|
||||
+1
@@ -0,0 +1 @@
|
||||
/** */
|
||||
+1
@@ -0,0 +1 @@
|
||||
/** */
|
||||
+1
@@ -0,0 +1 @@
|
||||
/** */
|
||||
+1
@@ -0,0 +1 @@
|
||||
/** */
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => (
|
||||
<div data-testid="root">root route {children}</div>
|
||||
),
|
||||
} as Route
|
||||
|
||||
const parent = {
|
||||
component: ({ children }: Props) => (
|
||||
<div data-testid="parent">parent route {children}</div>
|
||||
),
|
||||
options: {
|
||||
getParentRoute: () => root,
|
||||
},
|
||||
} as Route
|
||||
|
||||
const route = {
|
||||
component: () => <p data-testid="route">current route</p>,
|
||||
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(<RouteMatch route={route} serverSideProps={{}} />)
|
||||
expect(screen.getByTestId('root')).toHaveTextContent(
|
||||
'root route parent route current route',
|
||||
)
|
||||
expect(screen.getByTestId('route')).toHaveTextContent('current route')
|
||||
})
|
||||
})
|
||||
@@ -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 (
|
||||
<Root.component data={data} isLoading={isLoading}>
|
||||
<React.Suspense>
|
||||
<route.options.component data={data} isLoading={isLoading} />
|
||||
</React.Suspense>
|
||||
</Root.component>
|
||||
)
|
||||
}
|
||||
const routes = React.useMemo(() => {
|
||||
const components = loadParentComponents(route)
|
||||
components.push(route)
|
||||
return components
|
||||
}, [route.id])
|
||||
|
||||
return (
|
||||
<React.Suspense>
|
||||
<route.options.component data={data} isLoading={isLoading} />
|
||||
</React.Suspense>
|
||||
<TraverseRootComponents routes={routes} data={data} isLoading={isLoading} />
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Parent data={data} isLoading={isLoading}>
|
||||
{Boolean(routes.length > index) && (
|
||||
<TraverseRootComponents
|
||||
routes={routes}
|
||||
data={data}
|
||||
isLoading={isLoading}
|
||||
index={index + 1}
|
||||
/>
|
||||
)}
|
||||
</Parent>
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user