generouted
Version:
Generated client-side file-based routes for Vite
538 lines (364 loc) โข 16.9 kB
Markdown
<br>
<p align="center"><img src="https://raw.githubusercontent.com/oedotme/generouted/main/logo.svg" alt="generouted" width="280"/></p>
<p align="center">
<a href="https://npmjs.com/package/generouted"><img src="https://img.shields.io/npm/v/generouted.svg"/></a>
<a href="https://npmjs.com/package/generouted"><img src="https://img.shields.io/npm/l/generouted.svg?color=blue"/></a>
<a href="https://stackblitz.com/github.com/oedotme/generouted/tree/main/explorer"><img src="https://img.shields.io/badge/generouted/explorer-StackBlitz-blue"/></a>
</p>
<p align="center">
<a href="https://npmjs.com/package/generouted"><img src="https://img.shields.io/npm/dm/generouted.svg"/></a>
<a href="https://npmjs.com/package/generouted"><img src="https://img.shields.io/npm/dt/generouted.svg"/></a>
</p>
<br>
# Generouted
Generated file-based routes for [Vite](https://vitejs.dev)
<details>
<summary><b>Motivation</b></summary>
<br>
I enjoyed using file-based routing since I tried Next.js (pages directory). After applying the same concept with Vite and client-side applications, I started writing blog posts covering the implementation of [client-side file-based routing with React Router](https://omarelhawary.me/blog/file-based-routing-with-react-router) which was packaged later as `generouted`.
Today `generouted` brings many features, supports multiple frameworks and routers, and inspires ideas and conventions from Next.js, Remix, Expo, Docusaurus, SvelteKit and more.
<br>
</details>
<details>
<summary><b>How does it work?</b></summary>
<br>
`generouted` uses [Vite's glob import API](https://vitejs.dev/guide/features.html#glob-import) to list the routes within the `src/pages` directory and generates the routes tree and modals based on `generouted`'s conventions.
There are also Vite plugins available for some integrations to provide type-safe components/hooks/utils through code-generation.
<br>
</details>
<br>
## Features
- ๐ Client-side file-based routing
- โก Powered by [Vite](https://vitejs.dev)
- โจ React support with [`react-router`](https://github.com/remix-run/react-router) or [`@tanstack/router`](https://github.com/tanstack/router) ๐งช or [`@tanstack/react-location`](https://github.com/tanstack/router/tree/9c8eb043e4ac350fc1d28655542e01defb0c82e5) ๐จ
- โจ Solid support with [`@solidjs/router`](https://github.com/solidjs/solid-router)
- โจ File-based MDX routes with React or Solid, requires [`@mdx-js/rollup`](https://mdxjs.com/packages/rollup) [installation and setup](/examples/react-router-mdx)
- ๐ Type-safe navigation
- ๐ Type-safe global modals
- ๐ค Route-based code-splitting and lazy-loading
- ๐ Route-based data loaders and actions
- ๐ฃ Route-based error boundary
- ๐ Nested layouts
- ๐ซ Pathless layout groups
- โ Optional static and dynamic routes
- ๐ญ Ignored routes per file or directory
<br>
## Online explorer
- โก Visit [`generouted`'s interactive playground via StackBlitz](https://stackblitz.com/github.com/oedotme/generouted/tree/main/explorer)
- ๐งฉ Explore file-based routing patterns and conventions
- ๐ Visualize the routes layouts and the resolved routes paths
- ๐ Update `src/pages/` files and see your changes reflecting
<br>
## Getting started
<details open="true">
<summary><b>React Router</b></summary>
### React Router
In case you don't have a Vite project with React and TypeScript, check [Vite documentation to start a new project](https://vitejs.dev/guide/#scaffolding-your-first-vite-project).
#### Installation
```shell
pnpm add @generouted/react-router react-router
```
#### Setup
```ts
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import generouted from '@generouted/react-router/plugin'
export default defineConfig({ plugins: [react(), generouted()] })
```
#### Usage
```tsx
// src/main.tsx
import { createRoot } from 'react-dom/client'
import { Routes } from '@generouted/react-router'
createRoot(document.getElementById('root')!).render(<Routes />)
```
#### Adding pages
Add the home page by creating a new file `src/pages/index.tsx` โ `/`, then export a default component:
```tsx
export default function Home() {
return <h1>Home</h1>
}
```
Check the [routing conventions section below](#conventions).
#### Docs
You can find more details about type-safe navigation and global modals at [`@generouted/react-router` docs](/packages/react-router).
#### Examples
- [Type-safe navigation + global modals](/examples/react-router)
- [Custom integration](/examples/react-router-custom)
- [Custom integration with custom path](/examples/react-router-custom-path)
- [MDX routes](/examples/react-router-mdx)
<br>
</details>
<details open="true">
<summary><b>Solid Router</b></summary>
### Solid Router
In case you don't have a Vite project with Solid and TypeScript, check [Vite documentation to start a new project](https://vitejs.dev/guide/#scaffolding-your-first-vite-project).
#### Installation
```shell
pnpm add @generouted/solid-router @solidjs/router
```
#### Setup
```ts
// vite.config.ts
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
import generouted from '@generouted/solid-router/plugin'
export default defineConfig({ plugins: [solid(), generouted()] })
```
#### Usage
```tsx
// src/main.tsx
import { render } from 'solid-js/web'
import { Routes } from '@generouted/solid-router'
render(Routes, document.getElementById('root')!)
```
#### Adding pages
Add the home page by creating a new file `src/pages/index.tsx` โ `/`, then export a default component:
```tsx
export default function Home() {
return <h1>Home</h1>
}
```
See more about `generouted` [routing conventions below](#conventions).
#### Docs
You can find more details about type-safe navigation and global modals at [`@generouted/solid-router` docs](/packages/solid-router).
#### Examples
- [Type-safe navigation + global modals](/examples/solid-router)
<br>
</details>
<details>
<summary><b>TanStack React Router โ In-progress experimental support ๐งช</b></summary>
### TanStack React Router โ In-progress experimental support ๐งช
[Check out the docs here](/packages/tanstack-react-router)
#### Examples
- [Basic](/examples/tanstack-react-router)
<br>
</details>
<details>
<summary><b>React Location โ Deprecated ๐จ</b></summary>
### React Location โ Deprecated ๐จ
In case you don't have a Vite project with React and TypeScript, check [Vite documentation to start a new project](https://vitejs.dev/guide/#scaffolding-your-first-vite-project).
#### Installation
```shell
pnpm add generouted @tanstack/react-location
```
#### Usage
```tsx
// src/main.tsx
import { createRoot } from 'react-dom/client'
import { Routes } from 'generouted/react-location'
createRoot(document.getElementById('root')!).render(<Routes />)
```
#### Adding pages
Add the home page by creating a new file `src/pages/index.tsx` โ `/`, then export a default component:
```tsx
export default function Home() {
return <h1>Home</h1>
}
```
#### Examples
- [Basic](/examples/react-location/basic)
- [Data loaders](/examples/react-location/data-loaders)
- [Modals](/examples/react-location/modals)
- [Nested layouts](/examples/react-location/nested-layouts)
<br>
</details>
<br>
## Conventions
### File and directories naming and conventions
- Routes declaration at `src/pages`
- Supports `.tsx`, `.jsx` and `.mdx` file extensions
- Optional `src/pages/_app.tsx` for an **app level layout**
- Optional `src/pages/404.tsx` for a **custom not-found page**
#### Index routes
- `src/pages/index.tsx` โ `/`
- `src/pages/posts/index.tsx` โ `/posts`
#### Nested routes
- `src/pages/posts/2022/index.tsx` โ `/posts/2022`
- `src/pages/posts/2022/resolutions.tsx` โ `/posts/2022/resolutions`
#### Dynamic routes
- `src/pages/posts/[slug].tsx` โ `/posts/:slug`
- `src/pages/posts/[slug]/tags.tsx` โ `/posts/:slug/tags`
- `src/pages/posts/[...all].tsx` โ `/posts/*`
#### Nested layouts
- By defining `_layout.tsx` in any nested directory โ `src/pages/posts/_layout.tsx`
- **Requires** using an `<Outlet />` component to render layout children
- All the files within the `src/pages/posts/` directory in this case, will be wrapped with that layout
#### Nested URLs without nested layouts
- Route file should be outside of the nested layout directory
- Include **dots** `.` between the segments to be converted to forward slashes
- `src/pages/posts.nested.as.url.not.layout.tsx` โ `/posts/nested/as/url/not/layout`
#### Pathless layouts
- Similar to nested layouts for layout definition
- By wrapping the parent directory with **parentheses** `()`
- `src/pages/(auth)/_layout.tsx`
- `src/pages/(auth)/login.tsx` โ `/login`
- Layout parent directory name is not included in the routes paths
#### Global modals
- By **prefixing** the file name with a **plus sign** `+` _(thinking the modal is an extra route overlaying the current route)_
- Modals navigation available via the `useModals()` hook
- `src/pages/+info.tsx` โ `/info`
- `src/pages/+checkout/+details.tsx` โ `/checkout/details`
- `src/pages/+checkout/+payment.tsx` โ `/checkout/payment`
#### Optional segments
- By **prefixing** a route segment with a **minus sign** `-` _(thinking the segment can be subtracted or removed from the route path)_
- `src/pages/-en/about.tsx` โ `/en?/about` โ `/en/about`, `/about`
- `src/pages/-[lang]/about.tsx` โ `/:lang?/about` โ `/en/about`, `/fr/about`, `/about`
#### Ignored routes
- Any directory or file starts with an **underscore** `_` will be **ignored**
- `src/pages/_ignored.tsx`
- `src/pages/posts/_components/button.tsx`
- `src/pages/posts/_components/link.tsx`
<br>
### Page exports
- **Required** page component `export default Component() {...}`
- Optional page loader function `export const Loader = () => {...}`
- Optional page action function `export const Action = () => {...}`
- Optional suspense-based pending component `export const Pending = () => {...}`
- Optional error boundary component `export const Catch = () => {...}`
<br>
### Example
<details>
<summary><b>Directory structure</b></summary>
<br>
```shell
src/pages
โโโ (auth)
โ โโโ _layout.tsx
โ โโโ login.tsx
โ โโโ register.tsx
โโโ blog
โ โโโ _components
โ โ โโโ button.tsx
โ โ โโโ comments.tsx
โ โโโ [...all].tsx
โ โโโ [slug].tsx
โ โโโ _layout.tsx
โ โโโ index.tsx
โ โโโ tags.tsx
โโโ docs
โ โโโ -[lang]
โ โ โโโ index.tsx
โ โ โโโ resources.tsx
โ โโโ -en
โ โโโ contributors.tsx
โโโ +info.tsx
โโโ 404.tsx
โโโ _app.tsx
โโโ _ignored.tsx
โโโ about.tsx
โโโ blog.w.o.layout.tsx
โโโ index.tsx
```
</details>
<details open="true">
<summary><b>Overview</b></summary>
<br>
| File | Path | Convention |
| :------------------------------ | :----------------------- | :------------------------------------ |
| `(auth)/_layout.tsx` | | Pathless Layout group |
| `(auth)/login.tsx` | `/login` | Regular route |
| `(auth)/register.tsx` | `/register` | Regular route |
| `blog/_components/button.tsx` | | Ignored route by an ignored directory |
| `blog/_components/comments.tsx` | | Ignored route by an ignored directory |
| `blog/[...all].tsx` | `/blog/*` | Dynamic catch-all route |
| `blog/[slug].tsx` | `/blog/:slug` | Dynamic route |
| `blog/_layout.tsx` | | Layout for `/blog` routes |
| `blog/index.tsx` | `/blog` | Index route |
| `blog/tags.tsx` | `/blog/tags` | Regular route |
| `docs/-[lang]/index.tsx` | `/docs/:lang?/index` | Optional dynamic route segment |
| `docs/-[lang]/resources.tsx` | `/docs/:lang?/resources` | Optional dynamic route segment |
| `docs/-en/contributors.tsx` | `/docs/en?/contributors` | Optional static route segment |
| `+info.tsx` | `/info` | Modal route |
| `404.tsx` | `*` | Custom `404` _(optional)_ |
| `_app.tsx` | | Custom `app` layout _(optional)_ |
| `_ignored.tsx` | | Ignored route |
| `about.tsx` | `/about` | Regular route |
| `blog.w.o.layout.tsx` | `/blog/w/o/layout` | Route without `/blog` layout |
| `index.tsx` | `/` | Index route |
</details>
<br>
## API
### Routing
Via [`@generouted/react-router`](/packages/react-router) or [`@generouted/solid-router`](/packages/solid-router)
- `<Routes />` โ file-based routing component to be render in the app entry
- `routes` โ file-based routes tree/object used by default at `<Routes />` component
### Routing + code-splitting and lazy-loading
Via `@generouted/react-router/lazy` or `@generouted/solid-router/lazy`
- Used instead of `@generouted/react-router` or `@generouted/solid-router` to enable lazy-loading
- Make sure to replace all imports to lazy imports โ namely at app entry and `src/pages/_app.tsx`
- Provides the same `<Routes />` and `routes` exports
### Plugins
Via `@generouted/react-router/plugin` or `@generouted/solid-router/plugin`
- Vite plugin for type generation and initializing type-safe components/hooks/utils
- Generates `src/router.ts` file
- Exports type-safe `<Link>`, `<Navigate>`, `useModals()`, `useNavigate()`, `useParams()`, `redirect()`, etc.
- Check out [`@generouted/react-router` docs](/packages/react-router) or [`@generouted/solid-router` docs](/packages/solid-router) for more details
### Core
Via `@generouted/react-router/core` or `@generouted/solid-router/core`
- Available for customization, however it's recommended to use the available integrations directory via the `<Routes/>` component
- Check out the [custom integration example](/examples/react-router-custom)
<br>
## FAQ
<details>
<summary><b>How to implement protected or guarded routes?</b></summary>
<br>
There are multiple approaches to achieve that. If you prefer handling the logic in one place, you can define the protected routes with redirection handling within a component:
```tsx
// src/config/redirects.tsx
import { Navigate, useLocation } from 'react-router'
import { useAuth } from '../context/auth'
import { Path } from '../router'
const PRIVATE: Path[] = ['/logout']
const PUBLIC: Path[] = ['/login']
export const Redirects = ({ children }: { children: React.ReactNode }) => {
const auth = useAuth()
const location = useLocation()
const authenticatedOnPublicPath = auth.active && PUBLIC.includes(location.pathname as Path)
const unAuthenticatedOnPrivatePath = !auth.active && PRIVATE.includes(location.pathname as Path)
if (authenticatedOnPublicPath) return <Navigate to="/" replace />
if (unAuthenticatedOnPrivatePath) return <Navigate to="/login" replace />
return children
}
```
Then use that component (`<Redirects>` ) at the root-level layout `src/pages/_app.tsx` to wrap the `<Outlet>` component:
```tsx
// src/pages/_app.tsx
import { Outlet } from 'react-router'
import { Redirects } from '../config/redirects'
export default function App() {
return (
<section>
<header>
<nav>...</nav>
</header>
<main>
<Redirects>
<Outlet />
</Redirects>
</main>
</section>
)
}
```
You can find a full example of this approach at [Render template](https://github.com/oedotme/render/blob/main/src/config/redirects.tsx)
<br>
</details>
<details>
<summary><b>How to use with Hash or Memory Routers?</b></summary>
<br>
You can use the exported `routes` object to customize the router or to use hash/memory routers:
```tsx
import { createRoot } from 'react-dom/client'
import { RouterProvider, createHashRouter } from 'react-router'
import { routes } from '@generouted/react-router'
const router = createHashRouter(routes)
const Routes = () => <RouterProvider router={router} />
createRoot(document.getElementById('root')!).render(<Routes />)
```
<br>
</details>
<br>
## License
MIT