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 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
- Node HTTP server, and write its loaders, actions and middleware as Effects.
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` and `react-router` are peer dependencies. The library requires
32
- Effect 4.1 (`effect@^4.1.0`). Only `react-router-effect/vite` imports `@effect/platform-node`.
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/basic`](examples/basic) is an app with these files. It also has development with
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
- `ReactRouterHttp.make` returns an Effect that makes an Effect HTTP app. The app runs React Router
69
- with your route factory.
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 { ReactRouterHttp } from "react-router-effect/server";
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 reactRouterApp = ReactRouterHttp.make({
78
- build: () => import("virtual:react-router/server-build"),
79
- factory: routeFactory,
80
- clientAssets: true, // serves build/client in production
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
- The server makes the app once, when it starts, and serves it with its own Effect HTTP server and its
87
- own middleware. In production, making the app loads the build and reads the client build. If the
88
- server cannot read the client build, it fails to start, and the error names the resolved directory.
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
- ```ts
91
- // server/main.ts
101
+ ```js
102
+ // server.js
92
103
  import { createServer } from "node:http";
93
104
 
94
- import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
95
- import { Effect, Layer } from "effect";
96
- import { HttpMiddleware, HttpServer } from "effect/http";
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 { reactRouterApp } from "./app";
110
+ import { App } from "./build/server/index.js";
99
111
 
100
- Layer.unwrap(
101
- Effect.map(reactRouterApp, (app) => HttpServer.serve(app, HttpMiddleware.logger)),
102
- ).pipe(
103
- Layer.provide(NodeHttpServer.layer(() => createServer(), { port: 3000 })),
104
- Layer.launch,
105
- NodeRuntime.runMain,
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/main.ts` the entry of the server build. The build then contains your server, and
112
- `react-router build` writes it to `build/server/index.js`.
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/main.ts" } } } },
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 build/server/index.js
139
+ NODE_ENV=production node server.js # React's server build reads NODE_ENV
128
140
  ```
129
141
 
130
- For development, read [Development with Vite](#development-with-vite).
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
- The adapter option `requestContext` builds the services before React Router runs. Every middleware,
259
- loader and action sees them, and no middleware has to set them:
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
- ReactRouterHttp.make({
263
- build: () => import("virtual:react-router/server-build"),
264
- factory: routeFactory,
265
- requestContext: (request) =>
266
- Context.make(RequestInfo, {
267
- id: request.headers.get("x-request-id") ?? crypto.randomUUID(),
268
- startedAt: Date.now(),
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 Node HTTP server
314
- (`@effect/platform-node`). It handles everything between an `HttpServerRequest` and React Router. That
315
- is the Web `Request` and its abort signal, the route factory, and the client build. It also converts
316
- React Router's `Response`, with its streamed body and its cookies. The adapter is an
317
- Effect HTTP app like any other. The app serves it with its own Effect HTTP server and keeps its own
318
- middleware: logging, compression, tracing, proxies and other routes.
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 options:
329
-
330
- - `build` is the server build, or a function that loads it.
331
- - `factory` is the route factory.
332
- - `requestContext: (request) => Context` builds the request's services before React Router runs.
333
- - `clientAssets: true` serves the client build, `build.assetsBuildDirectory` under
334
- `build.publicPath`, before React Router. Files under `assets/` use a content hash in their name,
335
- and the adapter caches them for a year as immutable. It caches other files for an hour. It serves
336
- the files as `express.static` does. A directory serves its index file, such as a prerendered page.
337
- A directory path without the trailing slash redirects to the path with it. It does not serve
338
- dotfiles, and a request it does not serve goes to React Router. Like `express.static`, it looks up
339
- each GET and HEAD request in the file system. It serves only a build that it loads once: in
340
- production mode, or a build value. It never serves a development build, where Vite serves the
341
- client. `assetsBuildDirectory` is relative to the working directory of the server. If the adapter
342
- cannot read it, making the app fails with a `ClientBuildError` that names the resolved directory.
343
- It needs the platform's file system, which `NodeHttpServer.layer` provides.
344
- - `mode` is React Router's server mode. The default is `process.env.NODE_ENV`. In production the
345
- adapter loads the build once.
346
-
347
- The Web request comes from `HttpServerRequest.toWeb`, body included. Effect's stream sends the
348
- `100 Continue` when React Router reads the body of a request that sends `Expect: 100-continue`.
349
-
350
- The request URL is the one Effect derives from the request. An app behind a proxy applies the
351
- headers it trusts in its own middleware, with `request.modify` or `HttpMiddleware.xForwardedHeaders`.
352
- The request signal aborts only if the response ends early. That happens when the client leaves while
353
- React Router runs, or while the body streams. A completed response never aborts it.
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
- React Router logs and answers the errors of route code. If the adapter's own steps fail, for example
356
- a `requestContext` that throws, the adapter logs the error with `Effect.logError` and answers 500.
357
- The log goes to the loggers of the app's Effect runtime.
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
- ### Development with Vite
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
- In development Vite serves the client. `react-router-effect/vite` runs Vite's middleware in front of
362
- the app, on Effect's Node HTTP server:
437
+ ### Routes and middleware
363
438
 
364
- ```ts
365
- import { ReactRouterVite } from "react-router-effect/vite";
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
- const vite = await createServer({ server: { middlewareMode: true } });
368
- HttpServer.serve(Effect.flatten(reactRouterApp).pipe(ReactRouterVite.devMiddleware(vite)));
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
- In development, making the app does nothing but return it, so the server can make it for each
372
- request. Load the module that calls `ReactRouterHttp.make` through `vite.ssrLoadModule`. Then the route
373
- modules, the factory and the adapter share Vite's module graph. The example's `server.js` and
374
- `server/main.ts` do this.
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. Effect HTTP middleware around the
381
- adapter can read and change them like any other cookie. A response that replaces React Router's has
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`. If one response sets the
386
- same cookie name twice, for example to expire it on one path and set it on another, only the last
387
- one is sent.
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. The span comes from
405
- `HttpMiddleware.tracer` with a tracer that exports spans, such as `OtelTracer`. Each middleware,
406
- loader and action runs in a span of its own, such as `react-router.loader routes/todos`. The spans
407
- nest as React Router calls the functions. This is the example's `GET /todos`:
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, React Router 8.3 or later for the adapter, and `skipLibCheck`. The brand
514
- augments `Effect`, and TypeScript cannot test the variance annotations of that augmentation.
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 there is no `requestContext` option. Set the context key in a plain React Router
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 `ReactRouterHttp.make`, and the types `RouteEffectCheck` and
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.devMiddleware`, for development. It needs
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-MgSnnR33.mjs";
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
- const isUserError = (e) => typeof e === "object" && e !== null && "_tag" in e && e._tag in userHandlers;
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 (isUserError(e)) {
201
- const out = userHandlers[e._tag](e);
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) }));