react-router-effect 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +286 -108
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +4 -3
- package/dist/{route-data-MgSnnR33.d.mts → route-data-CWBxnNAA.d.mts} +7 -1
- package/dist/server.d.mts +140 -74
- package/dist/server.mjs +286 -140
- package/dist/vite.d.mts +35 -14
- package/dist/vite.mjs +47 -13
- package/package.json +12 -7
- package/src/assets.ts +212 -0
- package/src/check.ts +8 -0
- package/src/factory.ts +20 -10
- package/src/server-entry.ts +2 -1
- package/src/server.ts +307 -295
- package/src/vite.ts +109 -32
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# react-router-effect
|
|
2
2
|
|
|
3
3
|
Serve a [React Router](https://reactrouter.com) framework-mode app from [Effect](https://effect.website)'s
|
|
4
|
-
|
|
4
|
+
HTTP router, on Node or on Cloudflare Workers. Write its loaders, actions and middleware as Effects.
|
|
5
5
|
|
|
6
6
|
A route function that returns an `Effect` runs with your app's services. When it fails with a tagged
|
|
7
7
|
error, the route factory you configure once decides what happens: recover with data for
|
|
@@ -24,17 +24,21 @@ If your app does not serve React Router from an Effect HTTP server, read
|
|
|
24
24
|
|
|
25
25
|
## Install
|
|
26
26
|
|
|
27
|
+
On Node:
|
|
28
|
+
|
|
27
29
|
```bash
|
|
28
|
-
vp add react-router-effect effect @effect/platform-node
|
|
30
|
+
vp add react-router-effect effect react-router @effect/platform-node
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
`effect`, `@effect/platform-node`
|
|
32
|
-
Effect 4.1 (`effect@^4.1.0`). Only `react-router-effect/vite` imports
|
|
33
|
+
`effect`, `react-router` and `@effect/platform-node` are peer dependencies. The library requires
|
|
34
|
+
Effect 4.1 (`effect@^4.1.0`) and Node 22 or later. Only `react-router-effect/vite` imports
|
|
35
|
+
`@effect/platform-node`, so another platform does not need it. The README of each example lists the
|
|
36
|
+
packages of its platform. Read [Platforms](#platforms).
|
|
33
37
|
|
|
34
38
|
## Get started
|
|
35
39
|
|
|
36
|
-
[`examples/
|
|
37
|
-
Vite, tests against the production build, and benchmarks.
|
|
40
|
+
These steps serve an app on Node in production. [`examples/node`](examples/node) is an app with
|
|
41
|
+
these files. It also has development with Vite, tests against the production build, and benchmarks.
|
|
38
42
|
|
|
39
43
|
### 1. Create the route factory
|
|
40
44
|
|
|
@@ -65,51 +69,59 @@ export const { Respond } = routeFactory;
|
|
|
65
69
|
|
|
66
70
|
### 2. Create the server app
|
|
67
71
|
|
|
68
|
-
`
|
|
69
|
-
|
|
72
|
+
`App` is a layer of routes on Effect's `HttpRouter`. `ReactRouter.layer` loads the build with your
|
|
73
|
+
route factory. `ReactRouter.routes` registers React Router as the catch-all route.
|
|
74
|
+
`ReactRouterAssets.layer` registers the files of the client build. Your own routes and middleware go
|
|
75
|
+
in the same `Layer.mergeAll`.
|
|
70
76
|
|
|
71
77
|
```ts
|
|
72
78
|
// server/app.ts
|
|
73
|
-
import {
|
|
79
|
+
import { Layer } from "effect";
|
|
80
|
+
import { ReactRouter, ReactRouterAssets } from "react-router-effect/server";
|
|
74
81
|
|
|
75
82
|
import { routeFactory } from "../app/lib/route-factory.server";
|
|
76
83
|
|
|
77
|
-
export const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
export const App = Layer.mergeAll(ReactRouter.routes, ReactRouterAssets.layer).pipe(
|
|
85
|
+
Layer.provide(
|
|
86
|
+
ReactRouter.layer({
|
|
87
|
+
build: () => import("virtual:react-router/server-build"),
|
|
88
|
+
factory: routeFactory,
|
|
89
|
+
mode: import.meta.env.MODE,
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
82
93
|
```
|
|
83
94
|
|
|
84
95
|
### 3. Serve it
|
|
85
96
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
97
|
+
`server.js` serves `App` with `HttpRouter.serve`, which also logs each request. When the server
|
|
98
|
+
starts, `App` loads the build and reads the client build. If the server cannot read the client
|
|
99
|
+
build, it fails to start with a `ClientBuildError` that names the resolved directory.
|
|
89
100
|
|
|
90
|
-
```
|
|
91
|
-
// server
|
|
101
|
+
```js
|
|
102
|
+
// server.js
|
|
92
103
|
import { createServer } from "node:http";
|
|
93
104
|
|
|
94
|
-
import
|
|
95
|
-
import
|
|
96
|
-
import {
|
|
105
|
+
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
|
|
106
|
+
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
107
|
+
import { Layer } from "effect";
|
|
108
|
+
import { HttpRouter } from "effect/http";
|
|
97
109
|
|
|
98
|
-
import {
|
|
110
|
+
import { App } from "./build/server/index.js";
|
|
99
111
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
).pipe(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
NodeRuntime.runMain(
|
|
113
|
+
Layer.launch(
|
|
114
|
+
HttpRouter.serve(App).pipe(
|
|
115
|
+
Layer.provide(NodeHttpServer.layer(() => createServer(), { port: 3000 })),
|
|
116
|
+
),
|
|
117
|
+
),
|
|
106
118
|
);
|
|
107
119
|
```
|
|
108
120
|
|
|
109
121
|
### 4. Build the server
|
|
110
122
|
|
|
111
|
-
Make `server/
|
|
112
|
-
`
|
|
123
|
+
Make `server/app.ts` the entry of the server build. `react-router build` then writes `App` to
|
|
124
|
+
`build/server/index.js`.
|
|
113
125
|
|
|
114
126
|
```ts
|
|
115
127
|
// vite.config.ts
|
|
@@ -118,16 +130,17 @@ import { defineConfig } from "vite";
|
|
|
118
130
|
|
|
119
131
|
export default defineConfig({
|
|
120
132
|
plugins: [reactRouter()],
|
|
121
|
-
environments: { ssr: { build: { rollupOptions: { input: "./server/
|
|
133
|
+
environments: { ssr: { build: { rollupOptions: { input: "./server/app.ts" } } } },
|
|
122
134
|
});
|
|
123
135
|
```
|
|
124
136
|
|
|
125
137
|
```bash
|
|
126
138
|
react-router build
|
|
127
|
-
NODE_ENV=production node
|
|
139
|
+
NODE_ENV=production node server.js # React's server build reads NODE_ENV
|
|
128
140
|
```
|
|
129
141
|
|
|
130
|
-
For development,
|
|
142
|
+
For development with Vite, the Node example's `server.js` serves the app through
|
|
143
|
+
`ReactRouterVite.layer`. Read [Development with Vite](examples/node/README.md#development-with-vite).
|
|
131
144
|
|
|
132
145
|
### 5. Write routes as Effects
|
|
133
146
|
|
|
@@ -164,6 +177,28 @@ middleware](#plain-effect-loaders-actions-and-middleware) explains why. A route
|
|
|
164
177
|
not return an Effect passes through to React Router unchanged, so promise loaders keep working in
|
|
165
178
|
the same app.
|
|
166
179
|
|
|
180
|
+
## Platforms
|
|
181
|
+
|
|
182
|
+
`App` is the same layer on every platform. Only the file that serves it changes. Effect has two
|
|
183
|
+
functions that serve it:
|
|
184
|
+
|
|
185
|
+
- `HttpRouter.serve(App)` with a server layer, such as `NodeHttpServer.layer` on Node.
|
|
186
|
+
- `HttpRouter.toWebHandler(App)`, which returns a fetch handler, such as a Cloudflare Worker's.
|
|
187
|
+
|
|
188
|
+
| Platform | Example | Serves `App` with |
|
|
189
|
+
| ------------------ | -------------------------------------------- | --------------------------------------------- |
|
|
190
|
+
| Node | [`examples/node`](examples/node) | `HttpRouter.serve` and `NodeHttpServer.layer` |
|
|
191
|
+
| Cloudflare Workers | [`examples/cloudflare`](examples/cloudflare) | `HttpRouter.toWebHandler` |
|
|
192
|
+
|
|
193
|
+
The README of each example has the details of its platform. These rules apply on every platform:
|
|
194
|
+
|
|
195
|
+
- App-wide services go in the route factory's `runtime`. Read
|
|
196
|
+
[App-wide services with a runtime](#app-wide-services-with-a-runtime).
|
|
197
|
+
- Per-request services go in a router middleware. Read [Per-request services](#per-request-services).
|
|
198
|
+
- A platform service, such as a tracer, goes where the serve function reads it. `HttpRouter.serve`
|
|
199
|
+
reads the services that `Layer.provide` gives to its layer. `HttpRouter.toWebHandler` reads the
|
|
200
|
+
services that `Layer.provideMerge` adds to the layer that it gets.
|
|
201
|
+
|
|
167
202
|
## Handle errors
|
|
168
203
|
|
|
169
204
|
An Effect can short-circuit in three ways, with the `Respond` helpers:
|
|
@@ -255,19 +290,37 @@ makeEffectRouteFactory()(() => ({}));
|
|
|
255
290
|
The runtime provides app-wide services. A request-scoped service is the current user, a request id,
|
|
256
291
|
or a transaction for one request. There are two ways to provide one.
|
|
257
292
|
|
|
258
|
-
|
|
259
|
-
loader and action sees them, and no middleware has to set
|
|
293
|
+
A route-scoped router middleware on `ReactRouter.routes` provides services to React Router's
|
|
294
|
+
requests. Every middleware, loader and action sees them, and no React Router middleware has to set
|
|
295
|
+
them:
|
|
260
296
|
|
|
261
297
|
```ts
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
298
|
+
// server/app.ts
|
|
299
|
+
const RequestServices = HttpRouter.middleware((app) =>
|
|
300
|
+
Effect.provideServiceEffect(
|
|
301
|
+
app,
|
|
302
|
+
RequestInfo,
|
|
303
|
+
HttpServerRequest.HttpServerRequest.use((request) =>
|
|
304
|
+
Effect.succeed({
|
|
305
|
+
id: request.headers["x-request-id"] ?? crypto.randomUUID(),
|
|
306
|
+
startedAt: Date.now(),
|
|
307
|
+
}),
|
|
308
|
+
),
|
|
309
|
+
),
|
|
310
|
+
).layer;
|
|
311
|
+
|
|
312
|
+
export const App = Layer.mergeAll(
|
|
313
|
+
ReactRouter.routes.pipe(Layer.provide(RequestServices)),
|
|
314
|
+
ReactRouterAssets.layer,
|
|
315
|
+
).pipe(
|
|
316
|
+
Layer.provide(
|
|
317
|
+
ReactRouter.layer({
|
|
318
|
+
build: () => import("virtual:react-router/server-build"),
|
|
319
|
+
factory: routeFactory,
|
|
320
|
+
mode: import.meta.env.MODE,
|
|
269
321
|
}),
|
|
270
|
-
|
|
322
|
+
),
|
|
323
|
+
);
|
|
271
324
|
```
|
|
272
325
|
|
|
273
326
|
A [middleware](https://reactrouter.com/how-to/middleware) can also provide services. An effect
|
|
@@ -308,14 +361,24 @@ A route Effect can then require the runtime's services and the request's service
|
|
|
308
361
|
requirement is a compile error. `RequestContextKey<ReqServices>` is an alias of
|
|
309
362
|
`RouterContext<Context.Context<ReqServices>>`.
|
|
310
363
|
|
|
364
|
+
The adapter sets this key to the request fiber's context before React Router runs. That context
|
|
365
|
+
holds the server span, `HttpServerRequest` and the services of the router middleware. Do not set the
|
|
366
|
+
key to a new context in a React Router middleware, as an app without the adapter does. A new context
|
|
367
|
+
replaces those services for every function after that middleware. Provide request services with a
|
|
368
|
+
router middleware or an effect middleware, as this section shows.
|
|
369
|
+
|
|
311
370
|
## The adapter
|
|
312
371
|
|
|
313
|
-
`react-router-effect/server` is a React Router adapter for Effect's
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
372
|
+
`react-router-effect/server` is a React Router adapter for Effect's HTTP router, on every platform
|
|
373
|
+
that the router runs on. It is the Effect counterpart of `@react-router/express`. It
|
|
374
|
+
handles everything between an `HttpServerRequest` and React Router. That is the Web `Request` and
|
|
375
|
+
its abort signal, the route factory, and the client build. It also converts React Router's
|
|
376
|
+
`Response`, with its streamed body and its cookies. The app keeps its own server, middleware and
|
|
377
|
+
routes.
|
|
378
|
+
|
|
379
|
+
The adapter's API is unstable, and a minor version can change it. Every export of
|
|
380
|
+
`react-router-effect/server` and `react-router-effect/vite` has the JSDoc tag `@stability unstable`.
|
|
381
|
+
The route factory and the other exports of `react-router-effect` are stable.
|
|
319
382
|
|
|
320
383
|
React Router keeps doing what it does for any other server adapter: documents, `.data` requests,
|
|
321
384
|
streamed promises, resource routes, the lazy route manifest and middleware. Before React Router
|
|
@@ -325,66 +388,165 @@ sees the build, the adapter runs every loader, action and middleware through you
|
|
|
325
388
|
Its failures go through the factory's error handlers.
|
|
326
389
|
- Any other route function passes through. Promise loaders and `makeLoader` wrappers keep working.
|
|
327
390
|
|
|
328
|
-
The
|
|
329
|
-
|
|
330
|
-
- `
|
|
331
|
-
- `
|
|
332
|
-
- `
|
|
333
|
-
- `
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
391
|
+
The layers:
|
|
392
|
+
|
|
393
|
+
- `ReactRouter.layer(options)` provides the `ReactRouter` service. Its options are these:
|
|
394
|
+
- `build` is the server build, or a function that loads it.
|
|
395
|
+
- `factory` is the route factory.
|
|
396
|
+
- `mode` is React Router's server mode, and you must give it. In a Vite app, pass
|
|
397
|
+
`import.meta.env.MODE`. React Router runs in production mode for any mode but `development`
|
|
398
|
+
and `test`, and so does the layer. At layer build, the layer loads a build value once, or a
|
|
399
|
+
build function in production mode. In `development` and `test`, a build function loads for
|
|
400
|
+
each request.
|
|
401
|
+
- `loadContext` makes React Router's load context for each request. Read
|
|
402
|
+
[The load context](#the-load-context).
|
|
403
|
+
- `ReactRouter.routes` registers React Router as the catch-all route on `*`.
|
|
404
|
+
- `ReactRouterAssets.layer` registers the files of the client build. Read
|
|
405
|
+
[The client build](#the-client-build).
|
|
406
|
+
- `ReactRouterVite.layer`, from `react-router-effect/vite`, serves the app in development on Node.
|
|
407
|
+
Read [Development with Vite](examples/node/README.md#development-with-vite).
|
|
408
|
+
|
|
409
|
+
Provide one value of `ReactRouter.layer` to every layer that reads the service, as `App` does with
|
|
410
|
+
`Layer.provide`. Two values load the build twice.
|
|
411
|
+
|
|
412
|
+
### The load context
|
|
413
|
+
|
|
414
|
+
`loadContext` does for the adapter what `getLoadContext` does for `@react-router/express`. It
|
|
415
|
+
returns an Effect of a `RouterContextProvider`. Plain loaders and actions, and `handleRequest` in
|
|
416
|
+
`entry.server.tsx`, read its values with `context.get`. Route Effects read services from their
|
|
417
|
+
Effect context. So give route Effects services with router middleware, not with `loadContext`.
|
|
354
418
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
419
|
+
```ts
|
|
420
|
+
ReactRouter.layer({
|
|
421
|
+
build: () => import("virtual:react-router/server-build"),
|
|
422
|
+
factory: routeFactory,
|
|
423
|
+
mode: import.meta.env.MODE,
|
|
424
|
+
loadContext: (request) =>
|
|
425
|
+
Effect.sync(() => {
|
|
426
|
+
const context = new RouterContextProvider();
|
|
427
|
+
context.set(localeKey, request.headers["accept-language"] ?? "en");
|
|
428
|
+
return context;
|
|
429
|
+
}),
|
|
430
|
+
});
|
|
431
|
+
```
|
|
358
432
|
|
|
359
|
-
|
|
433
|
+
The Effect gets the services of the layer and the services of the request. The services of the
|
|
434
|
+
request, such as its scope, win. The adapter adds its own keys to the provider. If the Effect dies,
|
|
435
|
+
the client gets an empty 500.
|
|
360
436
|
|
|
361
|
-
|
|
362
|
-
the app, on Effect's Node HTTP server:
|
|
437
|
+
### Routes and middleware
|
|
363
438
|
|
|
364
|
-
|
|
365
|
-
|
|
439
|
+
The route that a request matches decides which middleware runs on it. Effect has three kinds of
|
|
440
|
+
middleware:
|
|
441
|
+
|
|
442
|
+
- A server middleware, the `middleware` option of `HttpRouter.serve`, wraps every request. It also
|
|
443
|
+
wraps the send of the response, so a change to the response with `Effect.map` does not reach the
|
|
444
|
+
client. `HttpMiddleware.compression` works there.
|
|
445
|
+
- A global router middleware, `HttpRouter.middleware(fn, { global: true })`, wraps every route.
|
|
446
|
+
- A route-scoped middleware, `HttpRouter.middleware(fn).layer`, applies only to the routes of the
|
|
447
|
+
layers that you provide it to.
|
|
366
448
|
|
|
367
|
-
|
|
368
|
-
|
|
449
|
+
Route-scoped middleware selects the middleware of each kind of request. `HttpRouter.serve` logs
|
|
450
|
+
every request. To leave file requests out of that log, provide `HttpRouter.disableLogger` to the
|
|
451
|
+
assets layer only:
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
export const App = Layer.mergeAll(
|
|
455
|
+
ReactRouter.routes.pipe(Layer.provide(RequestServices)),
|
|
456
|
+
ReactRouterAssets.layer.pipe(Layer.provide(HttpRouter.disableLogger)),
|
|
457
|
+
HttpRouter.middleware(HttpMiddleware.compression(), { global: true }),
|
|
458
|
+
).pipe(
|
|
459
|
+
Layer.provide(
|
|
460
|
+
ReactRouter.layer({
|
|
461
|
+
build: () => import("virtual:react-router/server-build"),
|
|
462
|
+
factory: routeFactory,
|
|
463
|
+
mode: import.meta.env.MODE,
|
|
464
|
+
}),
|
|
465
|
+
),
|
|
466
|
+
);
|
|
369
467
|
```
|
|
370
468
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
469
|
+
The order of two route-scoped middleware layers on one route is not fixed. If the order matters,
|
|
470
|
+
join them into one middleware with `combine`:
|
|
471
|
+
`HttpRouter.middleware(first).combine(HttpRouter.middleware(second)).layer`.
|
|
472
|
+
|
|
473
|
+
These rules apply to the routes:
|
|
474
|
+
|
|
475
|
+
- An exact route wins over a route with a parameter, and both win over a wildcard. So your own
|
|
476
|
+
routes and the files of the client build win over React Router's `*`.
|
|
477
|
+
- A route of your own on `*` or `/*`, for a method that React Router's route has, collides with it.
|
|
478
|
+
Then the server fails to start. A route of your own with the path of a file of the client build
|
|
479
|
+
fails the same way.
|
|
480
|
+
- If a HEAD request matches no HEAD route, it uses the GET route of its path. `ReactRouter.routes`
|
|
481
|
+
registers no HEAD route. So a GET route of your own answers HEAD, as it does in Express.
|
|
482
|
+
- React Router's route takes the methods of `HttpMethod.all`: GET, POST, PUT, DELETE, PATCH,
|
|
483
|
+
OPTIONS, TRACE and QUERY, and HEAD through GET. A request with another method, such as PROPFIND,
|
|
484
|
+
gets a 404 from the router.
|
|
485
|
+
- Register `ReactRouter.routes` at the root of the router. Under a prefix, React Router does not
|
|
486
|
+
get the path that it expects. To serve the app under a path, set React Router's `basename`.
|
|
487
|
+
- The router ignores the case of the path. On a file system that is case-sensitive, a request for a
|
|
488
|
+
file with the wrong case matches the file's route and gets a 404.
|
|
489
|
+
|
|
490
|
+
### The client build
|
|
491
|
+
|
|
492
|
+
`ReactRouterAssets.layer` serves `build.assetsBuildDirectory` under `build.publicPath`, as the two
|
|
493
|
+
`express.static` lines of React Router's Express template do. It needs the platform's file system,
|
|
494
|
+
which `NodeHttpServer.layer` provides. A Cloudflare Worker serves the client build from its static
|
|
495
|
+
assets, and does not use this layer.
|
|
496
|
+
|
|
497
|
+
- The hashed directory is the directory of the build's manifest URL, such as `/assets`. React
|
|
498
|
+
Router's build writes files with a content hash in their names there. The layer caches them for a
|
|
499
|
+
year as immutable. A request under the hashed directory gets a 404 for a file that the directory
|
|
500
|
+
does not have. A path with `..` does not get out of the hashed directory.
|
|
501
|
+
- Each other file gets its own route for GET and HEAD, and the layer caches it for an hour. At
|
|
502
|
+
server start, the layer reads the client build. If a file is added outside the hashed directory
|
|
503
|
+
later, the layer serves it only after a restart.
|
|
504
|
+
- A directory with an `index.html`, such as a prerendered page, serves that file. Its path without
|
|
505
|
+
the trailing slash redirects to the path with it. A root `index.html`, such as a prerendered home
|
|
506
|
+
page, serves `/`, and React Router's catch-all yields to it.
|
|
507
|
+
- If the manifest URL is not under `publicPath`, or it is at the root of `publicPath`, there is no
|
|
508
|
+
hashed directory. Then the layer caches every file for an hour.
|
|
509
|
+
- The layer serves no dotfiles, such as `.vite/manifest.json`, as `express.static` does.
|
|
510
|
+
- The layer registers nothing for a build that loads for each request, such as a development build.
|
|
511
|
+
Vite serves the client there. It also registers nothing for a `publicPath` on another origin.
|
|
512
|
+
- `assetsBuildDirectory` is relative to the working directory of the server. If the layer cannot
|
|
513
|
+
read it, the server fails to start with a `ClientBuildError` that names the resolved directory.
|
|
514
|
+
If a file name has a `*`, the server fails to start with a `ClientBuildError` too, because the
|
|
515
|
+
router cannot match that name.
|
|
516
|
+
|
|
517
|
+
### Requests
|
|
518
|
+
|
|
519
|
+
The Web request comes from `HttpServerRequest.toWeb`, body included. On Effect's Node server, the
|
|
520
|
+
request has the changes that middleware makes to the `HttpServerRequest`, such as the headers of
|
|
521
|
+
`HttpMiddleware.xForwardedHeaders`. On a Web server, such as a Cloudflare Worker, React Router gets
|
|
522
|
+
the runtime's own request. The README of each example describes the requests of its platform.
|
|
523
|
+
|
|
524
|
+
If the client leaves while React Router runs or while the body streams, the response ends early, and
|
|
525
|
+
the request signal aborts. A completed response never aborts it. On Cloudflare Workers, the abort
|
|
526
|
+
comes from Cloudflare's own request signal. For the conditions of that abort, read the
|
|
527
|
+
[Cloudflare example](examples/cloudflare/README.md#requests).
|
|
528
|
+
|
|
529
|
+
React Router logs and answers the errors of route code. If the adapter's own steps fail, for example
|
|
530
|
+
a `loadContext` that dies, the adapter logs the error with `Effect.logError`. Then it answers an
|
|
531
|
+
empty 500. The log goes to the loggers of the app's Effect runtime.
|
|
375
532
|
|
|
376
533
|
### Cookies
|
|
377
534
|
|
|
378
535
|
Set cookies from route code with React Router's cookie helpers, `createCookie` and the session
|
|
379
536
|
storages, as in any React Router app. The adapter converts React Router's `Set-Cookie` values into
|
|
380
|
-
the response's `cookies`, as `HttpServerResponse.fromWeb` does.
|
|
381
|
-
|
|
382
|
-
none of them.
|
|
537
|
+
the response's `cookies`, as `HttpServerResponse.fromWeb` does. A router middleware on
|
|
538
|
+
`ReactRouter.routes` can read and change them like any other cookie. A response that replaces React
|
|
539
|
+
Router's has none of them.
|
|
383
540
|
|
|
384
541
|
Effect keeps one cookie per name and only the attributes it knows: `Max-Age`, `Domain`, `Path`,
|
|
385
|
-
`Expires`, `HttpOnly`, `Secure`, `Partitioned`, `Priority` and `SameSite`.
|
|
386
|
-
same cookie name twice, for example to expire it on one path and set it on another
|
|
387
|
-
|
|
542
|
+
`Expires`, `HttpOnly`, `Secure`, `Partitioned`, `Priority` and `SameSite`. One response can set the
|
|
543
|
+
same cookie name twice, for example to expire it on one path and set it on another. Then Effect
|
|
544
|
+
sends only the last one.
|
|
545
|
+
|
|
546
|
+
Effect accepts only cookie names that are RFC 6265 tokens: letters, digits and
|
|
547
|
+
``!#$%&'*+-.^_`|~``. React Router's `createCookie` and session storages also accept other names,
|
|
548
|
+
such as `app/session` or `user:prefs`. The adapter does not send a cookie with such a name. It logs
|
|
549
|
+
an error with the cookie's name instead. Give every cookie a token name.
|
|
388
550
|
|
|
389
551
|
### Content type
|
|
390
552
|
|
|
@@ -401,10 +563,15 @@ return new Response(Readable.toWeb(file) as ReadableStream, {
|
|
|
401
563
|
|
|
402
564
|
### Tracing
|
|
403
565
|
|
|
404
|
-
When the request has a server span, the adapter traces React Router.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
566
|
+
When the request has a server span, the adapter traces React Router. If the app provides a tracer
|
|
567
|
+
that exports spans, such as `OtelTracer`, Effect's HTTP server makes the server span. Give the
|
|
568
|
+
tracer where the serve function reads it, as [Platforms](#platforms) tells. Effect serves each
|
|
569
|
+
request with the services of that layer. A tracer that the serve function does not read makes no
|
|
570
|
+
server span, so the adapter adds no spans. The README of each example shows where its tracer goes.
|
|
571
|
+
|
|
572
|
+
Each middleware, loader and action runs in a span of its own, such as
|
|
573
|
+
`react-router.loader routes/todos`.
|
|
574
|
+
The spans nest as React Router calls the functions. This is the example's `GET /todos`:
|
|
408
575
|
|
|
409
576
|
```text
|
|
410
577
|
GET the server span, with http.route=/todos
|
|
@@ -423,6 +590,12 @@ instrumentation traces nest there too, such as a database driver that a promise
|
|
|
423
590
|
Without a server span, or with Effect's native tracer, whose spans go nowhere, the adapter adds no
|
|
424
591
|
spans and no cost.
|
|
425
592
|
|
|
593
|
+
The adapter sets `http.route` on the server span to the pattern of the matched routes, such as
|
|
594
|
+
`/todos/:id`, once per request. It does this also for a page that runs no route function. A router
|
|
595
|
+
middleware that opens a span of its own does not change that: the pattern still goes on the server
|
|
596
|
+
span. A request that matches no route keeps the router's `*`. `ReactRouter.layer` reads the tracer
|
|
597
|
+
once, at layer build. So it does not see a tracer that the app provides only inside a request.
|
|
598
|
+
|
|
426
599
|
## Route functions
|
|
427
600
|
|
|
428
601
|
### Plain Effect loaders, actions and middleware
|
|
@@ -510,8 +683,10 @@ Type '{}' is missing the following properties from type ...:
|
|
|
510
683
|
|
|
511
684
|
### Requirements and limits
|
|
512
685
|
|
|
513
|
-
- Effect 4.1 or later,
|
|
514
|
-
|
|
686
|
+
- Effect 4.1 or later, and `skipLibCheck`. The brand augments `Effect`, and TypeScript cannot test
|
|
687
|
+
the variance annotations of that augmentation.
|
|
688
|
+
- For the adapter, React Router 8.3 or later. The route factory, `makeLoader`, `makeAction` and
|
|
689
|
+
`makeMiddleware` also support React Router 7.16 or later.
|
|
515
690
|
- A route module export must be a function. A bare `Effect` value is not a loader.
|
|
516
691
|
- Prerendering (`prerender`) and RSC mode call the build without the adapter.
|
|
517
692
|
|
|
@@ -594,9 +769,10 @@ of a rejected promise. It flows through the same recover and throw handling as a
|
|
|
594
769
|
|
|
595
770
|
### Per-request services
|
|
596
771
|
|
|
597
|
-
Without the adapter
|
|
598
|
-
middleware. The factory's `requestContext` reads it, and provides those services to the loader or
|
|
599
|
-
action
|
|
772
|
+
Without the adapter, no router middleware runs before React Router. Set the context key in a plain
|
|
773
|
+
React Router middleware. The factory's `requestContext` reads it, and provides those services to the loader or
|
|
774
|
+
action. With the adapter, do not set the key this way, as
|
|
775
|
+
[Per-request services](#per-request-services) tells. This is the pattern without the adapter:
|
|
600
776
|
|
|
601
777
|
```ts
|
|
602
778
|
// app/request-context.server.ts
|
|
@@ -651,10 +827,12 @@ export const loader = makeLoader(() =>
|
|
|
651
827
|
effect context.
|
|
652
828
|
- `EffectRouteArgs<Args>`, `EffectMiddlewareArgs<Middleware>`, `RouteData<F>` and `Register` are the
|
|
653
829
|
types of plain Effect routes.
|
|
654
|
-
- `react-router-effect/server` exports `
|
|
830
|
+
- `react-router-effect/server` (unstable) exports `ReactRouter`, with `ReactRouter.layer`, `ReactRouter.routes`
|
|
831
|
+
and the `ReactRouter.Options` type, and `ReactRouterAssets`, with `ReactRouterAssets.layer` and
|
|
832
|
+
`ReactRouterAssets.ClientBuildError`. It also exports the types `RouteEffectCheck` and
|
|
655
833
|
`RouteEffectFailures`.
|
|
656
|
-
- `react-router-effect/vite` exports `ReactRouterVite.
|
|
657
|
-
`@effect/platform-node`.
|
|
834
|
+
- `react-router-effect/vite` (unstable) exports `ReactRouterVite.layer` and the `ReactRouterVite.ViteDevServer`
|
|
835
|
+
type, for development on Node. It needs `@effect/platform-node`.
|
|
658
836
|
|
|
659
837
|
## Development
|
|
660
838
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as ReturnableDataError, D as WithActionMeta, E as isRouteError, O as actionMetaKey, S as AnyRouteError, T as ThrowableRedirectError, a as RegisteredMeta, b as RouteResult, c as Serialize, d as EffectNext, f as ErrorHandler, i as Register, l as ActionMetaArgs, n as EffectData, o as RegisteredRecover, p as FactoryTypes, r as EffectRouteArgs, s as RouteData, t as BrandData, u as EffectMiddlewareArgs, v as RequestContextKey, w as ThrowableDataError, x as makeEffectRouteFactory, y as RouteHandlers } from "./route-data-
|
|
1
|
+
import { C as ReturnableDataError, D as WithActionMeta, E as isRouteError, O as actionMetaKey, S as AnyRouteError, T as ThrowableRedirectError, a as RegisteredMeta, b as RouteResult, c as Serialize, d as EffectNext, f as ErrorHandler, i as Register, l as ActionMetaArgs, n as EffectData, o as RegisteredRecover, p as FactoryTypes, r as EffectRouteArgs, s as RouteData, t as BrandData, u as EffectMiddlewareArgs, v as RequestContextKey, w as ThrowableDataError, x as makeEffectRouteFactory, y as RouteHandlers } from "./route-data-CWBxnNAA.mjs";
|
|
2
2
|
export { type ActionMetaArgs, type AnyRouteError, type BrandData, type EffectData, type EffectMiddlewareArgs, type EffectNext, type EffectRouteArgs, type ErrorHandler, type FactoryTypes, type Register, type RegisteredMeta, type RegisteredRecover, type RequestContextKey, ReturnableDataError, type RouteData, type RouteHandlers, type RouteResult, type Serialize, ThrowableDataError, ThrowableRedirectError, type WithActionMeta, actionMetaKey, isRouteError, makeEffectRouteFactory };
|
package/dist/index.mjs
CHANGED
|
@@ -187,7 +187,8 @@ function makeEffectRouteFactory() {
|
|
|
187
187
|
...Respond
|
|
188
188
|
};
|
|
189
189
|
const userHandlers = config.errorHandlers ?? {};
|
|
190
|
-
|
|
190
|
+
/** The tags of the registered domain errors. */
|
|
191
|
+
const userTags = Object.keys(userHandlers);
|
|
191
192
|
/**
|
|
192
193
|
* Runs a handler's effect to completion: dispatches its whole error channel
|
|
193
194
|
* (registered domain errors, library route errors, respondables, or the 500
|
|
@@ -197,8 +198,8 @@ function makeEffectRouteFactory() {
|
|
|
197
198
|
*/
|
|
198
199
|
function runProgram(effect, context, signal) {
|
|
199
200
|
const program = effect.pipe(Effect.catchIf((_e) => true, (e) => {
|
|
200
|
-
if (
|
|
201
|
-
const out = userHandlers[
|
|
201
|
+
for (const tag of userTags) if (Predicate.isTagged(e, tag)) {
|
|
202
|
+
const out = userHandlers[tag](e);
|
|
202
203
|
return isRouteError(out) ? processRouteError(out) : out;
|
|
203
204
|
}
|
|
204
205
|
if (isRouteError(e)) return processRouteError(e);
|
|
@@ -209,6 +209,12 @@ type Diagnose<E, R, DomainError, Provided, AlsoHandled = never> = IsAny<E> exten
|
|
|
209
209
|
* it in middleware and pass it to the factory's `requestContext` - the runner
|
|
210
210
|
* reads it on every request and provides its services to the loader/action.
|
|
211
211
|
*
|
|
212
|
+
* With the server adapter, do not set it to a new context. The adapter sets it
|
|
213
|
+
* to the request fiber's context before React Router runs, and a new context
|
|
214
|
+
* replaces those services, such as `HttpServerRequest` and the server span.
|
|
215
|
+
* Provide request services with a router middleware on `ReactRouter.routes`.
|
|
216
|
+
* The middleware below is for an app without the adapter.
|
|
217
|
+
*
|
|
212
218
|
* It's just a `RouterContext`; create one with React Router's `createContext`:
|
|
213
219
|
*
|
|
214
220
|
* ```ts
|
|
@@ -222,7 +228,7 @@ type Diagnose<E, R, DomainError, Provided, AlsoHandled = never> = IsAny<E> exten
|
|
|
222
228
|
*
|
|
223
229
|
* export const requestContext: RequestContextKey<RequestContext> = createContext();
|
|
224
230
|
*
|
|
225
|
-
* // middleware:
|
|
231
|
+
* // middleware, in an app without the server adapter:
|
|
226
232
|
* export const middleware: Route.MiddlewareFunction[] = [
|
|
227
233
|
* ({ context, request }, next) => {
|
|
228
234
|
* context.set(requestContext, Context.make(RequestContext, { userId: readUser(request) }));
|