diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bec45cb..0624681 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -42,8 +42,9 @@ jobs: run: pnpm build - name: Upload coverage reports - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 if: matrix.node-version == '20.x' with: - files: ./coverage/coverage-final.json - fail_ci_if_error: false \ No newline at end of file + files: coverage/lcov.info + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index 279de37..87f3a83 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@
A tiny, fully type-safe composer for React Router loaders and actions
+A tiny, fully type-safe composer for React Router loaders, actions, and route guards.
@@ -12,8 +13,7 @@ React Router loaders and actions scale poorly once you start sharing logic. Context becomes implicit, middleware composition is manual, and type safety quickly degrades. -React Router Dataflow fixes this by letting you compose loaders and actions as -typed middleware pipelines, producing a fully type-safe context per route. +React Router Dataflow fixes this by letting you compose loaders, actions, and route guards as typed pipelines, producing a fully type-safe execution context per route. ```typescript import { Loader } from "react-router-dataflow"; @@ -65,7 +65,7 @@ const mw: LoaderMiddleware<{ data: string }> = async (args) => { Loader.with(mw); ``` -> Middlewares can also be typed action-only (`ActionMiddleware`) or universal (`Middleware`). +> Middlewares steps (for loader/action context pipelines) can also be typed action-only (`ActionMiddleware`) or universal (`Middleware`). ### Declare middlewares inline @@ -128,9 +128,44 @@ Action > For more advanced patterns such as context enforcement, parameterized middlewares and middleware factorization, see the [advanced middleware documentation](https://github.com/pheleine/react-router-dataflow/blob/master/docs/advanced_middlewares.md). +## RouteMiddleware - Guarding routes with React Router + +React Router Dataflow includes support for building **React Router middlewares** with a fully typed internal pipeline, using the same `with`/`build` builder API as loaders and actions. +`RouteMiddleware` is a builder that produces a **single React Router middleware** suitable for guarding routes and short-circuiting navigation. + +> `RouteMiddleware` is not related to middlewares types used by `Loader` and `Action` builders which are composing functions of loaders and actions. + +`RouteMiddleware` focuses on composing route guards by letting you compose a sequence of typed steps called **route requirements**. +Each requirement runs before the route is entered, can enrich internal context, and can throw a `Response` to block navigation. +Route requirements can be predefined in the same way as [loaders and actions middlewares](#define-reusable-middlewares) using `RouteRequirement` type. + +> `RouteMiddleware` does not support post-processing or response interception. +> If you need a pre/post middleware, write a React Router middleware directly and compose it alongside `RouteMiddleware`. + +> *Terminology* +> - **Middleware**: a loader/action pipeline step +> - **RouteMiddleware**: a builder that produces a React Router middleware +> - **RouteRequirement**: a typed guard step executed inside a RouteMiddleware + +```typescript +import { RouteMiddleware } from "react-router-dataflow"; +import { requireAuth } from "~/guards/require-auth"; + +// Ensures user is authenticated and has "admin" role +RouteMiddleware + .with(requireAuth) // => { user } + .build(async (_, { user }) => { + if (user.role !== "admin") { + throw new Response("Unauthorized", { status: 401 }); + } + + return null; + }); +``` + ## Integration in React Router -Loaders and actions created with React Router Dataflow are standard React Router data functions and behave the same way on the client and on the server. +Loaders, actions, and route middlewares created with React Router Dataflow are standard React Router primitives and behave the same way on the client and on the server. They can be used in: - framework mode (route files) - data routers (`createBrowserRouter`) @@ -139,21 +174,33 @@ They can be used in: ### Framework mode example ```tsx -// app/routes/example.tsx +// app/routes/data.$id.tsx import { Loader, Action } from "react-router-dataflow"; -import { requireAuth } from "~/middlewares/require-auth"; - -// Auth is enforced, no data is returned -export const loader = Loader.with(requireAuth).build(); - -// Auth is enforced, only PATCH/patch requests are handled -export const action = Action.with(requireAuth).build({ - PATCH: async ({ request }) => { - const updatedData = /* data update using request */; - return { updatedData }; - } -}); +import { requireAuth } from "~/guards/require-auth"; +import { dataFromParams } from "~/middlewares/data-from-params"; + +// Auth is enforced on the route +export const middleware = [ + RouteMiddleware + .with(requireAuth) + .build() +]; + +// => reached only if authenticated, get data from route params +export const loader = Loader + .with(dataFromParams) // => { data } + .build(async (_, { data }) => data); + +// => reached only if authenticated, only PATCH/patch requests are handled +export const action = Action + .with(dataFromParams) // => { data } + .build({ + PATCH: async ({ request }, { data }) => { + const updatedData = /* data update using request */; + return updatedData; + } + }); const ExamplePage = () =>