UNPKG

generouted

Version:

Generated client-side file-based routes for Vite

538 lines (364 loc) โ€ข 16.9 kB
<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