diff --git a/packages/tuono/README.md b/packages/tuono/README.md deleted file mode 120000 index 42061c01..00000000 --- a/packages/tuono/README.md +++ /dev/null @@ -1 +0,0 @@ -README.md \ No newline at end of file diff --git a/packages/tuono/package.json b/packages/tuono/package.json index 97743c50..c4b6e008 100644 --- a/packages/tuono/package.json +++ b/packages/tuono/package.json @@ -8,7 +8,7 @@ "lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc", "format": "prettier -u --write '**/*'", "types": "tsc --noEmit", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "vitest" }, "type": "module", "types": "dist/esm/index.d.ts", @@ -86,12 +86,16 @@ "@types/node": "^20.12.7", "@vitejs/plugin-react-swc": "^3.7.0", "fast-text-encoding": "^1.0.6", - "prettier": "^3.2.4", + "prettier": "^3.2.4", "vite": "^5.2.11", "zustand": "4.4.7" }, "devDependencies": { - "@types/babel-traverse": "^6.25.10" + "@testing-library/jest-dom": "^6.4.5", + "@testing-library/react": "^15.0.7", + "@types/babel-traverse": "^6.25.10", + "jsdom": "^24.0.0", + "vitest": "^1.5.2" }, "sideEffects": false, "keywords": [], diff --git a/packages/tuono/src/router/components/Matches.spec.tsx b/packages/tuono/src/router/components/Matches.spec.tsx new file mode 100644 index 00000000..74a3e3a0 --- /dev/null +++ b/packages/tuono/src/router/components/Matches.spec.tsx @@ -0,0 +1,38 @@ +import { afterEach, describe, expect, test, vi } from 'vitest' +import { getRouteByPathname } from './Matches' +import { cleanup } from '@testing-library/react' + +describe('Test getRouteByPathname fn', () => { + afterEach(() => { + cleanup() + }) + + test('match routes by ids', () => { + vi.mock('../hooks/useRouter.tsx', () => ({ + useRouter: (): { routesById: Record } => { + return { + routesById: { + '/': { id: '/' }, + '/about': { id: '/about' }, + '/posts/': { id: '/posts/' }, // posts/index + '/posts/[post]': { id: '/posts/[post]' }, + '/posts/defined-post': { id: '/posts/defined-post' }, + '/posts/[post]/[comment]': { id: '/posts/[post]/[comment]' }, + }, + } + }, + })) + + expect(getRouteByPathname('/')?.id).toBe('/') + expect(getRouteByPathname('/not-found')?.id).toBe(undefined) + expect(getRouteByPathname('/about')?.id).toBe('/about') + expect(getRouteByPathname('/posts/')?.id).toBe('/posts/') + expect(getRouteByPathname('/posts/dynamic-post')?.id).toBe('/posts/[post]') + expect(getRouteByPathname('/posts/defined-post')?.id).toBe( + '/posts/defined-post', + ) + expect(getRouteByPathname('/posts/dynamic-post/dynamic-comment')?.id).toBe( + '/posts/[post]/[comment]', + ) + }) +}) diff --git a/packages/tuono/src/router/components/Matches.tsx b/packages/tuono/src/router/components/Matches.tsx index 917e618e..9555c527 100644 --- a/packages/tuono/src/router/components/Matches.tsx +++ b/packages/tuono/src/router/components/Matches.tsx @@ -1,5 +1,6 @@ import { useRouter } from '../hooks/useRouter' import { useRouterStore } from '../hooks/useRouterStore' +import type { Route } from '../route' import { RouteMatch } from './RouteMatch' import NotFound from './NotFound' @@ -8,11 +9,54 @@ interface MatchesProps { serverSideProps: any } +const DYNAMIC_PATH_REGEX = /\[(.*?)\]/ + +export function getRouteByPathname(pathname: string): Route | undefined { + const { routesById } = useRouter() + + if (routesById[pathname]) return routesById[pathname] + + const dynamicRoutes = Object.keys(routesById).filter((route) => + DYNAMIC_PATH_REGEX.test(route), + ) + + if (!dynamicRoutes.length) return + + const pathSegments = pathname.split('/').filter(Boolean) + + let match = undefined + + // TODO: Check algo efficiency + for (const dynamicRoute of dynamicRoutes) { + const dynamicRouteSegments = dynamicRoute.split('/').filter(Boolean) + + const routeSegmentsCollector: string[] = [] + + for (let i = 0; i < dynamicRouteSegments.length; i++) { + if ( + dynamicRouteSegments[i] === pathSegments[i] || + DYNAMIC_PATH_REGEX.test(dynamicRouteSegments[i] || '') + ) { + routeSegmentsCollector.push(dynamicRouteSegments[i] ?? '') + } else { + break + } + } + + if (routeSegmentsCollector.length === pathSegments.length) { + match = `/${routeSegmentsCollector.join('/')}` + break + } + } + + if (!match) return + return routesById[match] +} + export function Matches({ serverSideProps }: MatchesProps): JSX.Element { const location = useRouterStore((st) => st.location) - const router = useRouter() - const route = router.routesById[location.pathname] + const route = getRouteByPathname(location.pathname) if (!route) { return diff --git a/packages/tuono/vite.config.ts b/packages/tuono/vite.config.ts index 6aa3a372..f929e672 100644 --- a/packages/tuono/vite.config.ts +++ b/packages/tuono/vite.config.ts @@ -1,3 +1,5 @@ +/// +/// import { defineConfig, mergeConfig } from 'vitest/config' import { tanstackBuildConfig } from '@tanstack/config/build' import react from '@vitejs/plugin-react' @@ -6,8 +8,9 @@ const config = defineConfig({ plugins: [react()], test: { name: 'react-router', - watch: false, + watch: true, environment: 'jsdom', + globals: true, }, })