mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
feat: create basic router
This commit is contained in:
@@ -97,7 +97,7 @@
|
||||
"no-prototype-builtins": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-shadow": "error",
|
||||
"no-undef": "error",
|
||||
"no-undef": "off",
|
||||
"sort-imports": "off",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -22,3 +22,13 @@ Why Tuono? Just a badass name.
|
||||
| - routes/
|
||||
| | - api/
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Data is passed by the loading function
|
||||
const IndexPage = ({data, isLoading, isError}) => <h1>Index Page</h1>
|
||||
|
||||
export const Route = createFileRoute({
|
||||
loader: (params) => fetchApi(params),
|
||||
component: IndexPage
|
||||
})
|
||||
```
|
||||
|
||||
+4
-2
@@ -2,9 +2,10 @@
|
||||
"name": "tuono",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"main": "src/index.js",
|
||||
"packageManager": "pnpm@8.12.1",
|
||||
"scripts": {
|
||||
"dev": "turbo dev",
|
||||
"build": "turbo build",
|
||||
"lint": "turbo lint",
|
||||
"format": "turbo format",
|
||||
@@ -17,6 +18,7 @@
|
||||
"devDependencies": {
|
||||
"@tanstack/config": "^0.7.0",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/react": "^18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
||||
"@typescript-eslint/parser": "^7.7.1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
@@ -28,7 +30,7 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"prettier": "^3.2.4",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.10",
|
||||
"vite": "^5.2.11",
|
||||
"vitest": "^1.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,44 @@
|
||||
{
|
||||
"name": "tuono-router",
|
||||
"version": "1.0.0",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/index.d.cts",
|
||||
"default": "./dist/cjs/index.cjs"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
|
||||
"format": "prettier -u --write '**/*'",
|
||||
"test": "vitest --watch=false",
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.3.0",
|
||||
"react-dom": ">=16.3.0"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"main": "dist/cjs/index.cjs",
|
||||
"module": "dist/esm/index.js",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"jsdom": "^24.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"zustand": "4.4.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useRouterStore } from '../hooks/useRouterStore'
|
||||
import type { AnchorHTMLAttributes, MouseEvent } from 'react'
|
||||
|
||||
export default function Link(
|
||||
props: AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
): JSX.Element {
|
||||
const handleTransition = (e: MouseEvent<HTMLAnchorElement>): void => {
|
||||
e.preventDefault()
|
||||
props.onClick?.(e)
|
||||
useRouterStore.setState({ location: { pathname: props.href || '' } })
|
||||
window.history.pushState('', '', props.href)
|
||||
}
|
||||
return (
|
||||
<a {...props} onClick={handleTransition}>
|
||||
{props.children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react'
|
||||
import { useRouter } from '../hooks/useRouter'
|
||||
import { useRouterStore } from '../hooks/useRouterStore'
|
||||
|
||||
export function Matches(): JSX.Element | undefined {
|
||||
const matchId = useRouterStore.getState().matches[0]?.id
|
||||
|
||||
//if (!matchId) return <></>
|
||||
|
||||
return (
|
||||
<React.Suspense>
|
||||
<Match id={matchId} />
|
||||
</React.Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
interface MatchProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
const Match = React.memo(function ({ id }: MatchProps) {
|
||||
const location = useRouterStore((st) => st.location)
|
||||
const router = useRouter()
|
||||
|
||||
console.log(router, location)
|
||||
|
||||
const route = router.routesById[location?.pathname]
|
||||
|
||||
return route.component()
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as React from 'react'
|
||||
|
||||
const Outlet = React.memo(function Outlet() {
|
||||
return <h1>Outlet</h1>
|
||||
})
|
||||
|
||||
export default Outlet
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react'
|
||||
import type { Router } from '../router'
|
||||
|
||||
const routerContext = React.createContext<Router>(null!)
|
||||
|
||||
const ROUTER_DOM_CONTEXT = '__TUONO_ROUTER__CONTEXT__'
|
||||
|
||||
export function getRouterContext(): any {
|
||||
if (typeof document === 'undefined') {
|
||||
return routerContext
|
||||
}
|
||||
|
||||
if (window[ROUTER_DOM_CONTEXT as any]) {
|
||||
return window[ROUTER_DOM_CONTEXT as any]
|
||||
}
|
||||
|
||||
window[ROUTER_DOM_CONTEXT as any] = routerContext as any
|
||||
|
||||
return routerContext
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import * as React from 'react'
|
||||
import { getRouterContext } from './RouterContext'
|
||||
import { Matches } from './Matches'
|
||||
|
||||
type Router = any
|
||||
|
||||
interface RouterContextProviderProps {
|
||||
router: Router
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function RouterContextProvider({
|
||||
router,
|
||||
children,
|
||||
...rest
|
||||
}: RouterContextProviderProps): JSX.Element {
|
||||
// Allow the router to update options on the router instance
|
||||
router.update({
|
||||
...router.options,
|
||||
...rest,
|
||||
context: {
|
||||
...router.options.context,
|
||||
...rest.context,
|
||||
},
|
||||
})
|
||||
|
||||
const routerContext = getRouterContext()
|
||||
|
||||
const pendingElement = router.options.defaultPendingComponent ? (
|
||||
<router.options.defaultPendingComponent />
|
||||
) : null
|
||||
|
||||
const provider = (
|
||||
<React.Suspense fallback={pendingElement}>
|
||||
<routerContext.Provider value={router}>{children}</routerContext.Provider>
|
||||
</React.Suspense>
|
||||
)
|
||||
|
||||
// NOTE: verify usefulness
|
||||
if (router.options.Wrap) {
|
||||
return <router.options.Wrap>{provider}</router.options.Wrap>
|
||||
}
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
interface RouterProviderProps {
|
||||
router: Router
|
||||
}
|
||||
|
||||
export function RouterProvider({
|
||||
router,
|
||||
...rest
|
||||
}: RouterProviderProps): JSX.Element {
|
||||
return (
|
||||
<RouterContextProvider router={router} {...rest}>
|
||||
<Matches />
|
||||
</RouterContextProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as React from 'react'
|
||||
import { getRouterContext } from '../components/RouterContext'
|
||||
import type { Router } from '../router'
|
||||
|
||||
export function useRouter(): Router {
|
||||
return React.useContext(getRouterContext())
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react'
|
||||
import { create } from 'zustand'
|
||||
|
||||
export interface ParsedLocation {
|
||||
href: string
|
||||
pathname: string
|
||||
search: Record<string, string>
|
||||
searchStr: string
|
||||
hash: string
|
||||
}
|
||||
|
||||
interface RouterState {
|
||||
isLoading: boolean
|
||||
isTransitioning: boolean
|
||||
status: 'idle'
|
||||
location?: ParsedLocation
|
||||
matches: string[]
|
||||
pendingMatches: string[]
|
||||
cachedMatches: string[]
|
||||
statusCode: 200
|
||||
updateLocation: (loc: ParsedLocation) => void
|
||||
}
|
||||
|
||||
export const useRouterStore = create<RouterState>()((set) => ({
|
||||
isLoading: false,
|
||||
isTransitioning: false,
|
||||
status: 'idle',
|
||||
location: {
|
||||
href: '',
|
||||
pathname: '/',
|
||||
search: {},
|
||||
searchStr: '',
|
||||
hash: '',
|
||||
},
|
||||
matches: [],
|
||||
pendingMatches: [],
|
||||
cachedMatches: [],
|
||||
statusCode: 200,
|
||||
updateLocation: (location: ParsedLocation): void => set({ location }),
|
||||
}))
|
||||
@@ -0,0 +1,4 @@
|
||||
export { RouterProvider } from './components/RouterProvider'
|
||||
export { default as Link } from './components/Link'
|
||||
export { createRouter } from './router'
|
||||
export { createRoute, createRootRoute, getRouteApi } from './route'
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { RouterType } from './router'
|
||||
import { trimPathLeft, joinPaths } from './utils'
|
||||
|
||||
interface RouteOptions {
|
||||
isRoot?: boolean
|
||||
getParentRoute?: () => Route
|
||||
path?: string
|
||||
component: ReactNode
|
||||
}
|
||||
|
||||
export function createRoute(options: RouteOptions): Route {
|
||||
return new Route(options)
|
||||
}
|
||||
|
||||
export const rootRouteId = '__root__'
|
||||
|
||||
export class Route {
|
||||
parentRoute!: any
|
||||
id: number
|
||||
fullPath!: string
|
||||
path: string
|
||||
options: any
|
||||
|
||||
children?: Route[]
|
||||
router: RouterType
|
||||
isRoot: boolean
|
||||
originalIndex: number
|
||||
component: ReactNode
|
||||
|
||||
constructor(options: RouteOptions) {
|
||||
this.isRoot = options.isRoot || !options.getParentRoute
|
||||
this.options = options
|
||||
;(this as any).$$typeof = Symbol.for('react.memo')
|
||||
|
||||
this.component = options.component
|
||||
}
|
||||
|
||||
init = (originalIndex: number): void => {
|
||||
this.originalIndex = originalIndex
|
||||
|
||||
const isRoot = !this.options?.path && !this.options?.id
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
this.parentRoute = this.options?.getParentRoute?.()
|
||||
|
||||
if (isRoot) {
|
||||
this.path = rootRouteId as TPath
|
||||
}
|
||||
|
||||
let path: undefined | string = isRoot ? rootRouteId : this.options.path
|
||||
|
||||
// If the path is anything other than an index path, trim it up
|
||||
if (path && path !== '/') {
|
||||
path = trimPathLeft(path)
|
||||
}
|
||||
|
||||
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,
|
||||
])
|
||||
|
||||
if (path === rootRouteId) {
|
||||
path = '/'
|
||||
}
|
||||
|
||||
if (id !== rootRouteId) {
|
||||
id = joinPaths(['/', id])
|
||||
}
|
||||
|
||||
const fullPath =
|
||||
id === rootRouteId ? '/' : joinPaths([this.parentRoute.fullPath, path])
|
||||
|
||||
this.path = path as TPath
|
||||
this.id = id as TId
|
||||
// this.customId = customId as TCustomId
|
||||
this.fullPath = fullPath as TFullPath
|
||||
this.to = fullPath as TrimPathRight<TFullPath>
|
||||
}
|
||||
|
||||
addChildren(routes: Route[]): Route {
|
||||
this.children = routes
|
||||
return this
|
||||
}
|
||||
|
||||
update = (options: RouteOptions): this => {
|
||||
Object.assign(this.options, options)
|
||||
return this
|
||||
}
|
||||
|
||||
useRouteContext = () => {}
|
||||
|
||||
useParams = () => {}
|
||||
}
|
||||
|
||||
export function createRootRoute(options?: RouteOptions): Route {
|
||||
return new Route({ ...options, isRoot: true })
|
||||
}
|
||||
|
||||
export function getRouteApi(id: string): RouteApi {
|
||||
return new RouteApi(id)
|
||||
}
|
||||
|
||||
class RouteApi {
|
||||
id: string
|
||||
|
||||
constructor(id: string) {
|
||||
this.id = id
|
||||
}
|
||||
|
||||
useParams = () => {}
|
||||
|
||||
useRouteContext = () => {}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { trimPath, trimPathRight, trimPathLeft, parsePathname } from './utils'
|
||||
import type { Route } from './route'
|
||||
|
||||
interface CreateRouter {
|
||||
routeTree: any
|
||||
basePath?: string
|
||||
}
|
||||
export type RouterType = any
|
||||
|
||||
export function createRouter(options: CreateRouterArgs): Router {
|
||||
return new Router(options)
|
||||
}
|
||||
|
||||
export class Router {
|
||||
options: any
|
||||
basePath = '/'
|
||||
routeTree: any
|
||||
history: any
|
||||
isServer = typeof document === 'undefined'
|
||||
routesById = {}
|
||||
routesByPath = {}
|
||||
flatRoutes: any
|
||||
|
||||
constructor(options: CreateRouter) {
|
||||
this.update({
|
||||
...options,
|
||||
})
|
||||
|
||||
if (!this.isServer) {
|
||||
;(window as any).__TSR__ROUTER__ = this
|
||||
}
|
||||
}
|
||||
|
||||
update = (newOptions: CreateRouter): void => {
|
||||
this.options = {
|
||||
...this.options,
|
||||
...newOptions,
|
||||
}
|
||||
|
||||
this.#updateBasePath(newOptions.basePath)
|
||||
|
||||
// NOTE: next iteration
|
||||
this.#historyUpdate()
|
||||
|
||||
// NOTE: next iteration
|
||||
this.#storeUpdate()
|
||||
|
||||
if (this.options.routeTree !== this.routeTree) {
|
||||
this.routeTree = this.options.routeTree
|
||||
this.#buildRouteTree()
|
||||
}
|
||||
}
|
||||
|
||||
#historyUpdate = (): void => {
|
||||
// TODO: update history
|
||||
}
|
||||
|
||||
#storeUpdate = (): void => {
|
||||
// TODO: update store
|
||||
}
|
||||
|
||||
#buildRouteTree = (): void => {
|
||||
const recurseRoutes = (childRoutes: Route[]): void => {
|
||||
childRoutes.forEach((route: Route, i: number) => {
|
||||
route.init(i)
|
||||
|
||||
this.routesById[route.id] = route
|
||||
|
||||
if (!route.isRoot && route.options.path) {
|
||||
const trimmedFullPath = trimPathRight(route.fullPath)
|
||||
if (
|
||||
!this.routesByPath[trimmedFullPath] ||
|
||||
route.fullPath.endsWith('/')
|
||||
) {
|
||||
this.routesByPath[trimmedFullPath] = route
|
||||
}
|
||||
}
|
||||
|
||||
const children = route.children
|
||||
if (children?.length) {
|
||||
recurseRoutes(children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
recurseRoutes([this.routeTree])
|
||||
}
|
||||
|
||||
#updateBasePath = (basePath?: string): void => {
|
||||
if (!this.basePath || (basePath && basePath !== basePath)) {
|
||||
if (basePath === undefined || basePath === '' || basePath === '/') {
|
||||
this.basePath = '/'
|
||||
} else {
|
||||
this.basePath = `/${trimPath(basePath)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface Segment {
|
||||
type: 'pathname' | 'param' | 'wildcard'
|
||||
value: string
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { Segment } from './types'
|
||||
|
||||
export function joinPaths(paths: (string | undefined)[]): string {
|
||||
return cleanPath(paths.filter(Boolean).join('/'))
|
||||
}
|
||||
|
||||
function cleanPath(path: string): string {
|
||||
// remove double slashes
|
||||
return path.replace(/\/{2,}/g, '/')
|
||||
}
|
||||
|
||||
export function parsePathname(pathname?: string): Segment[] {
|
||||
if (!pathname) {
|
||||
return []
|
||||
}
|
||||
|
||||
pathname = cleanPath(pathname)
|
||||
|
||||
const segments: Segment[] = []
|
||||
|
||||
if (pathname.slice(0, 1) === '/') {
|
||||
pathname = pathname.substring(1)
|
||||
segments.push({
|
||||
type: 'pathname',
|
||||
value: '/',
|
||||
})
|
||||
}
|
||||
|
||||
if (!pathname) {
|
||||
return segments
|
||||
}
|
||||
|
||||
// Remove empty segments and '.' segments
|
||||
const split = pathname.split('/').filter(Boolean)
|
||||
|
||||
segments.push(
|
||||
...split.map((part): Segment => {
|
||||
if (part === '$' || part === '*') {
|
||||
return {
|
||||
type: 'wildcard',
|
||||
value: part,
|
||||
}
|
||||
}
|
||||
|
||||
if (part.charAt(0) === '$') {
|
||||
return {
|
||||
type: 'param',
|
||||
value: part,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'pathname',
|
||||
value: part,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
if (pathname.slice(-1) === '/') {
|
||||
pathname = pathname.substring(1)
|
||||
segments.push({
|
||||
type: 'pathname',
|
||||
value: '/',
|
||||
})
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
export function trimPathLeft(path: string): string {
|
||||
return path === '/' ? path : path.replace(/^\/{1,}/, '')
|
||||
}
|
||||
export function trimPathRight(path: string): string {
|
||||
return path === '/' ? path : path.replace(/\/{1,}$/, '')
|
||||
}
|
||||
export function trimPath(path: string): string {
|
||||
return trimPathRight(trimPathLeft(path))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { expectTypeOf, test } from 'vitest'
|
||||
import {
|
||||
RouterProvider,
|
||||
createRootRoute,
|
||||
createRoute,
|
||||
createRouter,
|
||||
} from '../../src'
|
||||
|
||||
const rootRoute = createRootRoute()
|
||||
|
||||
const indexRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
const invoicesRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/invoices',
|
||||
})
|
||||
|
||||
const routeTree = rootRoute.addChildren([invoicesRoute, indexRoute])
|
||||
|
||||
const defaultRouter = createRouter({
|
||||
routeTree,
|
||||
})
|
||||
|
||||
type DefaultRouter = typeof defaultRouter
|
||||
|
||||
test('can pass default router to the provider', () => {
|
||||
expectTypeOf(RouterProvider).parameter(0).toMatchTypeOf<{
|
||||
router: DefaultRouter
|
||||
routeTree?: DefaultRouter['routeTree']
|
||||
}>()
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
/* eslint-disable */
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { getRouteApi, createRoute } from '../src'
|
||||
|
||||
describe('getRouteApi', () => {
|
||||
it('should have the useRouteContext hook', () => {
|
||||
const api = getRouteApi('foo')
|
||||
expect(api.useRouteContext).toBeDefined()
|
||||
})
|
||||
|
||||
it('should have the useParams hook', () => {
|
||||
const api = getRouteApi('foo')
|
||||
expect(api.useParams).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createRoute has the same hooks as getRouteApi', () => {
|
||||
const routeApi = getRouteApi('foo')
|
||||
const hookNames = Object.keys(routeApi).filter((key) => key.startsWith('use'))
|
||||
const route = createRoute({} as any)
|
||||
|
||||
it.each(hookNames.map((name) => [name]))(
|
||||
'should have the "%s" hook defined',
|
||||
(hookName) => {
|
||||
expect(route[hookName as keyof typeof route]).toBeDefined()
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"jsx": "react-jsx",
|
||||
},
|
||||
"include": ["src", "tests", "vite.config.ts"],
|
||||
}
|
||||
|
||||
@@ -18,4 +18,3 @@ export default mergeConfig(
|
||||
srcDir: './src',
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
|
||||
"format": "prettier -u --write '**/*'",
|
||||
@@ -28,7 +29,9 @@
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"prettier": "^3.2.4"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"main": "dist/cjs/index.cjs",
|
||||
|
||||
@@ -239,29 +239,40 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
(d): any => d,
|
||||
])
|
||||
|
||||
const imports = [
|
||||
...sortedRouteNodes.map((node) => {
|
||||
return `import ${node.variableName}Import from './${replaceBackslash(
|
||||
removeExt(
|
||||
path.relative(
|
||||
path.dirname(config.generatedRouteTree),
|
||||
path.resolve(config.folderName, node.filePath),
|
||||
),
|
||||
false,
|
||||
),
|
||||
)}'`
|
||||
}),
|
||||
].join('\n')
|
||||
|
||||
const createRoutes = [
|
||||
...sortedRouteNodes.map((node) => {
|
||||
return `const ${node.variableName} = createRoute({ component: ${node.variableName}Import })`
|
||||
}),
|
||||
].join('\n')
|
||||
|
||||
const routeImports = [
|
||||
'// This file is auto-generated by Tuono',
|
||||
"import { createRoute } from 'tuono'",
|
||||
[
|
||||
`import { Route as rootRoute } from './${replaceBackslash(
|
||||
`import RootImport from './${replaceBackslash(
|
||||
path.relative(
|
||||
path.dirname(config.generatedRouteTree),
|
||||
path.resolve(config.folderName, ROOT_PATH_ID),
|
||||
),
|
||||
)}'`,
|
||||
...sortedRouteNodes.map((node) => {
|
||||
return `import { Route as ${
|
||||
node.variableName
|
||||
}Import } from './${replaceBackslash(
|
||||
removeExt(
|
||||
path.relative(
|
||||
path.dirname(config.generatedRouteTree),
|
||||
path.resolve(config.folderName, node.filePath),
|
||||
),
|
||||
false,
|
||||
),
|
||||
)}'`
|
||||
}),
|
||||
].join('\n'),
|
||||
imports,
|
||||
'const rootRoute = createRoute({ isRoot: true, component: RootImport });',
|
||||
createRoutes,
|
||||
'// Create/Update Routes',
|
||||
sortedRouteNodes
|
||||
.map((node) => {
|
||||
@@ -269,7 +280,7 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
const lazyComponentNode = routePiecesByPath[node.routePath]?.lazy
|
||||
|
||||
return [
|
||||
`const ${node.variableName}Route = ${node.variableName}Import.update({
|
||||
`const ${node.variableName}Route = ${node.variableName}.update({
|
||||
${[
|
||||
`path: '${node.cleanedPath}'`,
|
||||
`getParentRoute: () => ${node.parent?.variableName ?? 'root'}Route`,
|
||||
@@ -302,25 +313,6 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
].join('')
|
||||
})
|
||||
.join('\n\n'),
|
||||
...[
|
||||
'// Populate the FileRoutesByPath interface',
|
||||
`declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
${routeNodes
|
||||
.map((routeNode) => {
|
||||
return `'${removeTrailingUnderscores(routeNode.routePath)}': {
|
||||
preLoaderRoute: typeof ${routeNode.variableName}Import
|
||||
parentRoute: typeof ${
|
||||
routeNode.parent?.variableName
|
||||
? `${routeNode.parent.variableName}Import`
|
||||
: 'rootRoute'
|
||||
}
|
||||
}`
|
||||
})
|
||||
.join('\n')}
|
||||
}
|
||||
}`,
|
||||
],
|
||||
'// Create and export the route tree',
|
||||
`export const routeTree = rootRoute.addChildren([${routeConfigChildrenText}])`,
|
||||
]
|
||||
|
||||
@@ -1,56 +1,43 @@
|
||||
// This file is auto-generated by Tuono
|
||||
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as AboutImport } from './routes/about'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as PostsIndexImport } from './routes/posts/index'
|
||||
import { Route as PostsPostSlugImport } from './routes/posts/post-slug'
|
||||
import { createRoute } from 'tuono'
|
||||
|
||||
import RootImport from './routes/__root'
|
||||
|
||||
import AboutImport from './routes/about'
|
||||
import IndexImport from './routes/index'
|
||||
import PostsIndexImport from './routes/posts/index'
|
||||
import PostsPostSlugImport from './routes/posts/post-slug'
|
||||
|
||||
const rootRoute = createRoute({ isRoot: true, component: RootImport })
|
||||
|
||||
const About = createRoute({ component: AboutImport })
|
||||
const Index = createRoute({ component: IndexImport })
|
||||
const PostsIndex = createRoute({ component: PostsIndexImport })
|
||||
const PostsPostSlug = createRoute({ component: PostsPostSlugImport })
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const AboutRoute = AboutImport.update({
|
||||
const AboutRoute = About.update({
|
||||
path: '/about',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const IndexRoute = IndexImport.update({
|
||||
const IndexRoute = Index.update({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const PostsIndexRoute = PostsIndexImport.update({
|
||||
const PostsIndexRoute = PostsIndex.update({
|
||||
path: '/posts/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const PostsPostSlugRoute = PostsPostSlugImport.update({
|
||||
const PostsPostSlugRoute = PostsPostSlug.update({
|
||||
path: '/posts/post-slug',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
preLoaderRoute: typeof IndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/about': {
|
||||
preLoaderRoute: typeof AboutImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/posts/post-slug': {
|
||||
preLoaderRoute: typeof PostsPostSlugImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/posts/': {
|
||||
preLoaderRoute: typeof PostsIndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export the route tree
|
||||
|
||||
export const routeTree = rootRoute.addChildren([
|
||||
|
||||
+13
-20
@@ -1,36 +1,29 @@
|
||||
// This file is auto-generated by Tuono
|
||||
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as AboutImport } from './routes/about'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { createRoute } from 'tuono'
|
||||
|
||||
import RootImport from './routes/__root'
|
||||
|
||||
import AboutImport from './routes/about'
|
||||
import IndexImport from './routes/index'
|
||||
|
||||
const rootRoute = createRoute({ isRoot: true, component: RootImport })
|
||||
|
||||
const About = createRoute({ component: AboutImport })
|
||||
const Index = createRoute({ component: IndexImport })
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const AboutRoute = AboutImport.update({
|
||||
const AboutRoute = About.update({
|
||||
path: '/about',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const IndexRoute = IndexImport.update({
|
||||
const IndexRoute = Index.update({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
preLoaderRoute: typeof IndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/about': {
|
||||
preLoaderRoute: typeof AboutImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export the route tree
|
||||
|
||||
export const routeTree = rootRoute.addChildren([IndexRoute, AboutRoute])
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"name": "tuono-vite",
|
||||
"version": "0.1.0",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
"main": "dist/cjs/index.cjs",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"module": "dist/esm/index.js",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
|
||||
"format": "prettier -u --write '**/*'",
|
||||
|
||||
@@ -40,7 +40,7 @@ export function RouterGenerator(): Plugin {
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'vite-plugin-fs-router-generator',
|
||||
name: 'vite-plugin-tuono-fs-router-generator',
|
||||
configResolved: async (): Promise<void> => {
|
||||
await generate()
|
||||
},
|
||||
@@ -59,7 +59,7 @@ export function RouterCodeSplitter(): Plugin {
|
||||
const ROOT: string = process.cwd()
|
||||
|
||||
return {
|
||||
name: 'vite-plugin-fs-router-code-splitter',
|
||||
name: 'vite-plugin-tuono-fs-router-code-splitter',
|
||||
enforce: 'pre',
|
||||
resolveId(source): string {
|
||||
if (source.startsWith(SPLIT_PREFIX + ':')) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
|
||||
"format": "prettier -u --write '**/*'",
|
||||
@@ -31,6 +32,9 @@
|
||||
"src",
|
||||
"README.md"
|
||||
],
|
||||
"dependencies": {
|
||||
"tuono-router": "workspace:*"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"keywords": [],
|
||||
"author": "Valerio Ageno",
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
export default function sum(a: number, b: number): number {
|
||||
return a + b
|
||||
}
|
||||
import {
|
||||
createRoute,
|
||||
createRootRoute,
|
||||
createRouter,
|
||||
Link,
|
||||
RouterProvider,
|
||||
} from 'tuono-router'
|
||||
|
||||
export { createRoute, createRootRoute, createRouter, Link, RouterProvider }
|
||||
|
||||
+5
-1
@@ -5,7 +5,11 @@
|
||||
"format": {},
|
||||
"test": {},
|
||||
"build": {
|
||||
"outputs": ["dist/**"]
|
||||
"outputs": ["dist/**"],
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"dev": {
|
||||
"dependsOn": ["^dev"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user