mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-29 22:02:46 -07:00
refactor(packages/*): add tuono- prefix to all relevant folders (#255)
This commit is contained in:
committed by
GitHub
parent
f236b75f55
commit
f3bf13da59
@@ -0,0 +1,72 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { buildRouteConfig } from './build-route-config'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
filePath: 'posts/my-post.tsx',
|
||||
fullPath:
|
||||
'/tuono/packages/tuono-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/tuono-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/tuono-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/tuono-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/tuono-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/tuono-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', () => {
|
||||
it('Should build the correct config', () => {
|
||||
const config = buildRouteConfig(routes)
|
||||
expect(config).toStrictEqual(
|
||||
'PostsMyPostRoute,PostsIndexRoute,PostspostIndexRoute',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
import { spaces } from './utils'
|
||||
import type { RouteNode } from './types'
|
||||
|
||||
export function buildRouteConfig(nodes: Array<RouteNode>, depth = 1): string {
|
||||
const children = nodes.map((node) => {
|
||||
const route = `${node.variableName as string}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(`,`)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const ROUTES_FOLDER = './src/routes/'
|
||||
export const ROOT_PATH_ID = '__root'
|
||||
export const GENERATED_ROUTE_TREE = './.tuono/routeTree.gen.ts'
|
||||
@@ -0,0 +1,262 @@
|
||||
import * as fsp from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
import { format } from 'prettier'
|
||||
|
||||
import { buildRouteConfig } from './build-route-config'
|
||||
import { hasParentRoute } from './has-parent-route'
|
||||
import {
|
||||
cleanPath,
|
||||
determineNodePath,
|
||||
multiSortBy,
|
||||
replaceBackslash,
|
||||
removeExt,
|
||||
routePathToVariable,
|
||||
removeGroups,
|
||||
removeLastSlash,
|
||||
removeUnderscores,
|
||||
} from './utils'
|
||||
|
||||
import type { Config, RouteNode } from './types'
|
||||
import { ROUTES_FOLDER, ROOT_PATH_ID, GENERATED_ROUTE_TREE } from './constants'
|
||||
|
||||
import { sortRouteNodes } from './sort-route-nodes'
|
||||
import isDefaultExported from './utils/is-default-exported'
|
||||
|
||||
let latestTask = 0
|
||||
|
||||
const defaultConfig: Config = {
|
||||
folderName: ROUTES_FOLDER,
|
||||
generatedRouteTree: GENERATED_ROUTE_TREE,
|
||||
}
|
||||
|
||||
let isFirst = false
|
||||
let skipMessage = false
|
||||
|
||||
async function getRouteNodes(
|
||||
config = defaultConfig,
|
||||
): Promise<{ routeNodes: Array<RouteNode>; rustHandlersNodes: Array<string> }> {
|
||||
const routeNodes: Array<RouteNode> = []
|
||||
const rustHandlersNodes: Array<string> = []
|
||||
|
||||
async function recurse(dir: string): Promise<void> {
|
||||
const fullDir = path.resolve(config.folderName, dir)
|
||||
const dirList = await fsp.readdir(fullDir, { withFileTypes: true })
|
||||
|
||||
await Promise.all(
|
||||
dirList.map(async (dirent) => {
|
||||
const fullPath = path.join(fullDir, dirent.name)
|
||||
const relativePath = path.join(dir, dirent.name)
|
||||
|
||||
if (dirent.isDirectory()) {
|
||||
await recurse(relativePath)
|
||||
} else if (fullPath.match(/\.(tsx|ts|jsx|js|mdx)$/)) {
|
||||
// Check that the route is correctly default exported
|
||||
if (
|
||||
fullPath.match(/\.(tsx|ts|jsx|js)$/) &&
|
||||
!isDefaultExported((await fsp.readFile(fullPath)).toString())
|
||||
) {
|
||||
return
|
||||
}
|
||||
const filePath = replaceBackslash(path.join(dir, dirent.name))
|
||||
const filePathNoExt = removeExt(filePath)
|
||||
let routePath = cleanPath(`/${filePathNoExt}`) || ''
|
||||
|
||||
const variableName = routePathToVariable(routePath)
|
||||
|
||||
// Remove the index from the route path and
|
||||
// if the route path is empty, use `/'
|
||||
if (routePath === 'index') {
|
||||
routePath = '/'
|
||||
}
|
||||
|
||||
routePath = routePath.replace(/\/index$/, '/') || '/'
|
||||
|
||||
routeNodes.push({
|
||||
filePath,
|
||||
fullPath,
|
||||
routePath,
|
||||
variableName,
|
||||
})
|
||||
} else if (fullPath.match(/\.(rs)$/)) {
|
||||
const filePath = replaceBackslash(path.join(dir, dirent.name))
|
||||
const filePathNoExt = removeExt(filePath)
|
||||
let routePath =
|
||||
cleanPath(`/${filePathNoExt.split('.').join('/')}`) || ''
|
||||
|
||||
if (routePath === 'index') {
|
||||
routePath = '/'
|
||||
}
|
||||
|
||||
routePath = routePath.replace(/\/index$/, '/') || '/'
|
||||
|
||||
rustHandlersNodes.push(routePath)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
await recurse('./')
|
||||
|
||||
return { routeNodes, rustHandlersNodes }
|
||||
}
|
||||
|
||||
export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
if (!isFirst) {
|
||||
isFirst = true
|
||||
} else if (skipMessage) {
|
||||
skipMessage = false
|
||||
}
|
||||
|
||||
const checkLatest = (): boolean => {
|
||||
if (latestTask !== taskId) {
|
||||
skipMessage = true
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const taskId = latestTask + 1
|
||||
latestTask = taskId
|
||||
|
||||
const { routeNodes: beforeRouteNodes, rustHandlersNodes } =
|
||||
await getRouteNodes(config)
|
||||
|
||||
const preRouteNodes = sortRouteNodes(beforeRouteNodes)
|
||||
|
||||
const routeNodes: Array<RouteNode> = []
|
||||
|
||||
// Loop over the flat list of routeNodes and
|
||||
// build up a tree based on the routeNodes' routePath
|
||||
const handleNode = (node: RouteNode): void => {
|
||||
const parentRoute = hasParentRoute(routeNodes, node, node.routePath)
|
||||
|
||||
if (parentRoute) node.parent = parentRoute
|
||||
|
||||
node.path = determineNodePath(node)
|
||||
|
||||
node.cleanedPath = removeLastSlash(
|
||||
removeGroups(removeUnderscores(node.path) ?? ''),
|
||||
)
|
||||
|
||||
if (node.parent) {
|
||||
node.parent.children = node.parent.children ?? []
|
||||
node.parent.children.push(node)
|
||||
}
|
||||
routeNodes.push(node)
|
||||
}
|
||||
|
||||
for (const node of preRouteNodes) {
|
||||
handleNode(node)
|
||||
}
|
||||
|
||||
const routeConfigChildrenText = buildRouteConfig(routeNodes)
|
||||
|
||||
const sortedRouteNodes = multiSortBy(routeNodes, [
|
||||
(d): number => (d.routePath.includes(`/${ROOT_PATH_ID}`) ? -1 : 1),
|
||||
(d): number => d.routePath.split('/').length,
|
||||
(d): number => (d.routePath.endsWith("index'") ? -1 : 1),
|
||||
(d): RouteNode => d,
|
||||
])
|
||||
|
||||
const imports = [
|
||||
...sortedRouteNodes.map((node) => {
|
||||
const extension = node.filePath.endsWith('mdx') ? '.mdx' : ''
|
||||
return `const ${
|
||||
node.variableName as string
|
||||
}Import = dynamic(() => import('./${replaceBackslash(
|
||||
removeExt(
|
||||
path.relative(
|
||||
path.dirname(config.generatedRouteTree),
|
||||
path.resolve(config.folderName, node.filePath),
|
||||
),
|
||||
false,
|
||||
),
|
||||
)}${extension}'))`
|
||||
}),
|
||||
].join('\n')
|
||||
|
||||
const createRoutes = [
|
||||
...sortedRouteNodes.map((node) => {
|
||||
const isRoot = node.routePath.endsWith(ROOT_PATH_ID)
|
||||
const rootDeclaration = isRoot ? ', isRoot: true' : ''
|
||||
const variableName = node.variableName as string
|
||||
|
||||
return `const ${variableName} = createRoute({ component: ${variableName}Import${rootDeclaration} })`
|
||||
}),
|
||||
].join('\n')
|
||||
|
||||
const createRouteUpdates = [
|
||||
sortedRouteNodes
|
||||
.map((node) => {
|
||||
const variableName = node.variableName as string
|
||||
const cleanedPath = node.cleanedPath as string
|
||||
return [
|
||||
`const ${variableName}Route = ${variableName}.update({
|
||||
${[
|
||||
!node.path?.endsWith(ROOT_PATH_ID) && `path: '${cleanedPath}'`,
|
||||
`getParentRoute: () => ${node.parent?.variableName ?? 'root'}Route`,
|
||||
rustHandlersNodes.includes(node.path || '')
|
||||
? 'hasHandler: true'
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(',')}
|
||||
})`,
|
||||
].join('')
|
||||
})
|
||||
.join('\n\n'),
|
||||
]
|
||||
|
||||
const routeImports = [
|
||||
'// This file is auto-generated by Tuono',
|
||||
"import { createRoute, dynamic } from 'tuono'",
|
||||
[
|
||||
`import RootImport from './${replaceBackslash(
|
||||
path.relative(
|
||||
path.dirname(config.generatedRouteTree),
|
||||
path.resolve(config.folderName, ROOT_PATH_ID),
|
||||
),
|
||||
)}'`,
|
||||
].join('\n'),
|
||||
imports,
|
||||
'const rootRoute = createRoute({ isRoot: true, component: RootImport });',
|
||||
createRoutes,
|
||||
'// Create/Update Routes',
|
||||
createRouteUpdates,
|
||||
'// Create and export the route tree',
|
||||
`export const routeTree = rootRoute.addChildren([${routeConfigChildrenText}])`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
|
||||
const routeConfigFileContent = await format(routeImports, {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
parser: 'typescript',
|
||||
})
|
||||
|
||||
const routeTreeContent = await fsp
|
||||
.readFile(path.resolve(config.generatedRouteTree), 'utf-8')
|
||||
.catch((e: unknown) => {
|
||||
const err = e as Error & { code?: string }
|
||||
if (err.code === 'ENOENT') {
|
||||
return undefined
|
||||
}
|
||||
throw err
|
||||
})
|
||||
|
||||
if (!checkLatest()) return
|
||||
|
||||
if (routeTreeContent !== routeConfigFileContent) {
|
||||
await fsp.mkdir(path.dirname(path.resolve(config.generatedRouteTree)), {
|
||||
recursive: true,
|
||||
})
|
||||
if (!checkLatest()) return
|
||||
await fsp.writeFile(
|
||||
path.resolve(config.generatedRouteTree),
|
||||
routeConfigFileContent,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { hasParentRoute } from './has-parent-route'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
filePath: 'posts/[post].tsx',
|
||||
fullPath:
|
||||
'/tuono/packages/tuono-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/tuono-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/tuono-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/tuono-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/tuono-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/tuono-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/tuono-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', () => {
|
||||
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: Array<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,47 @@
|
||||
import { normalize } from 'path'
|
||||
|
||||
import type { Plugin } from 'vite'
|
||||
|
||||
import { routeGenerator } from './generator'
|
||||
|
||||
const ROUTES_DIRECTORY_PATH = './src/routes'
|
||||
|
||||
let lock = false
|
||||
|
||||
export default function RouterGenerator(): Plugin {
|
||||
const generate = async (): Promise<void> => {
|
||||
if (lock) return
|
||||
lock = true
|
||||
|
||||
try {
|
||||
await routeGenerator()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
lock = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleFile = async (file: string): Promise<void> => {
|
||||
const filePath = normalize(file)
|
||||
|
||||
if (filePath.startsWith(ROUTES_DIRECTORY_PATH)) {
|
||||
await generate()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'vite-plugin-tuono-fs-router',
|
||||
configResolved: async (): Promise<void> => {
|
||||
await generate()
|
||||
},
|
||||
watchChange: async (
|
||||
file: string,
|
||||
context: { event: string },
|
||||
): Promise<void> => {
|
||||
if (['create', 'update', 'delete'].includes(context.event)) {
|
||||
await handleFile(file)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { sortRouteNodes } from './sort-route-nodes'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
filePath: 'index.tsx',
|
||||
fullPath:
|
||||
'/tuono/packages/tuono-fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/index.tsx',
|
||||
routePath: '/',
|
||||
variableName: 'Index',
|
||||
path: '/',
|
||||
cleanedPath: '/',
|
||||
},
|
||||
{
|
||||
filePath: 'about.tsx',
|
||||
fullPath:
|
||||
'/tuono/packages/tuono-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/tuono-fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/__root.tsx',
|
||||
routePath: '/__root',
|
||||
variableName: 'root',
|
||||
},
|
||||
{
|
||||
filePath: 'posts/[post].tsx',
|
||||
fullPath:
|
||||
'/tuono/packages/tuono-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/tuono-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/tuono-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/tuono-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/tuono-fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/index.tsx',
|
||||
routePath: '/',
|
||||
variableName: 'Index',
|
||||
path: '/',
|
||||
cleanedPath: '/',
|
||||
},
|
||||
{
|
||||
filePath: 'about.tsx',
|
||||
fullPath:
|
||||
'/tuono/packages/tuono-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/tuono-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/tuono-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/tuono-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/tuono-fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routes/posts/[post].tsx',
|
||||
routePath: '/posts/[post]',
|
||||
variableName: 'Postspost',
|
||||
},
|
||||
]
|
||||
|
||||
describe('sortRouteNodes works', () => {
|
||||
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: Array<RouteNode>): Array<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 || ''))
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface RouteNode {
|
||||
filePath: string
|
||||
fullPath: string
|
||||
routePath: string
|
||||
path?: string
|
||||
cleanedPath?: string
|
||||
isLayout?: boolean
|
||||
children?: Array<RouteNode>
|
||||
parent?: RouteNode
|
||||
variableName?: string
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
folderName: string
|
||||
generatedRouteTree: string
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { RouteNode } from './types'
|
||||
|
||||
export function removeExt(d: string, keepExtension: boolean = false): string {
|
||||
return keepExtension ? d : d.substring(0, d.lastIndexOf('.')) || d
|
||||
}
|
||||
|
||||
export function replaceBackslash(s: string): string {
|
||||
return s.replaceAll(/\\/gi, '/')
|
||||
}
|
||||
|
||||
export function cleanPath(pathToClean: string): string {
|
||||
// remove double slashes
|
||||
return pathToClean.replace(/\/{2,}/g, '/')
|
||||
}
|
||||
|
||||
export function removeUnderscores(s?: string): string | undefined {
|
||||
return s?.replaceAll(/(^_|_$)/gi, '').replaceAll(/(\/_|_\/)/gi, '/')
|
||||
}
|
||||
|
||||
export function routePathToVariable(routePath: string): string {
|
||||
return (
|
||||
removeUnderscores(routePath)
|
||||
?.replace(/\/\$\//g, '/splat/')
|
||||
.replace(/\$$/g, 'splat')
|
||||
.replace(/\$/g, '')
|
||||
.split(/[/-]/g)
|
||||
.map((d, i) => (i > 0 ? capitalize(d) : d))
|
||||
.join('')
|
||||
.replace(/([^a-zA-Z0-9]|[.])/gm, '')
|
||||
.replace(/^(\d)/g, 'R$1') ?? ''
|
||||
)
|
||||
}
|
||||
|
||||
export function multiSortBy<T>(
|
||||
arr: Array<T>,
|
||||
accessors: Array<(item: T) => unknown> = [(d): unknown => d],
|
||||
): Array<T> {
|
||||
return arr
|
||||
.map((d, i) => [d, i] as const)
|
||||
.sort(([a, ai], [b, bi]) => {
|
||||
for (const accessor of accessors) {
|
||||
const ao = accessor(a)
|
||||
const bo = accessor(b)
|
||||
|
||||
if (typeof ao === 'undefined') {
|
||||
if (typeof bo === 'undefined') {
|
||||
continue
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
if (ao === bo) {
|
||||
continue
|
||||
}
|
||||
|
||||
return (ao as number) > (bo as number) ? 1 : -1
|
||||
}
|
||||
|
||||
return ai - bi
|
||||
})
|
||||
.map(([d]) => d)
|
||||
}
|
||||
|
||||
export function capitalize(s: string): string {
|
||||
if (typeof s !== 'string') return ''
|
||||
return s.charAt(0).toUpperCase() + s.slice(1)
|
||||
}
|
||||
|
||||
export function spaces(d: number): string {
|
||||
return Array.from({ length: d })
|
||||
.map(() => ' ')
|
||||
.join('')
|
||||
}
|
||||
|
||||
export function removeTrailingUnderscores(s?: string): string | undefined {
|
||||
return s?.replaceAll(/(_$)/gi, '').replaceAll(/(_\/)/gi, '/')
|
||||
}
|
||||
|
||||
export function removeGroups(s: string): string {
|
||||
return s.replaceAll('//', '/')
|
||||
}
|
||||
|
||||
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.
|
||||
* @param node - The node to determine the path for.
|
||||
* @returns The correct path for the given node.
|
||||
*/
|
||||
export function determineNodePath(node: RouteNode): string {
|
||||
return (node.path = node.parent
|
||||
? node.routePath.replace(node.parent.routePath, '') || '/'
|
||||
: node.routePath)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default function isDefaultExported(source: string): boolean {
|
||||
const regex = new RegExp('export default')
|
||||
return regex.test(source)
|
||||
}
|
||||
Reference in New Issue
Block a user