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/src/vite.ts CHANGED
@@ -6,23 +6,36 @@
6
6
  * serves the client: route modules, `/@vite/client`, the files of `public/`,
7
7
  * and its error overlay. A custom server runs Vite in middleware mode and passes
8
8
  * it each request first. Vite's middleware is a Connect stack of Node
9
- * requests and responses, so this middleware needs Effect's Node HTTP server.
9
+ * requests and responses, so this layer needs Effect's Node HTTP server.
10
10
  *
11
+ * Its API is unstable: a minor version can change it.
12
+ *
13
+ * @stability unstable
11
14
  * @since 2.1.0
12
15
  */
13
16
  import type * as Http from "node:http";
14
17
 
15
18
  import * as NodeHttpServerRequest from "@effect/platform-node/NodeHttpServerRequest";
16
- import { Effect } from "effect";
17
- import { HttpServerRequest, HttpServerResponse } from "effect/http";
19
+ import { Effect, Exit, Layer, Option, Predicate, Scope, SynchronizedRef, Tuple } from "effect";
20
+ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
18
21
 
19
- /** The part of a Vite dev server the middleware uses: its Connect middleware stack. */
22
+ /**
23
+ * The parts of a Vite dev server that the adapter uses.
24
+ *
25
+ * @stability unstable
26
+ */
20
27
  export interface ViteDevServer {
28
+ /** Vite's Connect middleware stack. */
21
29
  readonly middlewares: (
22
30
  request: Http.IncomingMessage,
23
31
  response: Http.ServerResponse,
24
32
  next: (error?: unknown) => void,
25
33
  ) => void;
34
+ /**
35
+ * Maps the stack of an error to the source files, as the Express template
36
+ * does. Vite's dev server has it.
37
+ */
38
+ readonly ssrFixStacktrace?: ((error: Error) => void) | undefined;
26
39
  }
27
40
 
28
41
  /**
@@ -57,36 +70,100 @@ const passThroughVite = (
57
70
  });
58
71
 
59
72
  /**
60
- * Passes each request to Vite first, and the requests Vite passes on to the
61
- * app. Use it in development only, with Vite in middleware mode:
62
- *
63
- * ```ts
64
- * import { ReactRouterVite } from "react-router-effect/vite";
73
+ * Serves the app in development, as the Express template does. Give it to
74
+ * `HttpRouter.serve` in place of the app's `App`:
65
75
  *
76
+ * ```js
66
77
  * const vite = await createServer({ server: { middlewareMode: true } });
67
- * HttpServer.serve(Effect.flatten(reactRouterApp).pipe(ReactRouterVite.devMiddleware(vite)));
78
+ * const App = ReactRouterVite.layer(vite, () =>
79
+ * vite.ssrLoadModule("./server/app.ts").then((module) => module.App),
80
+ * );
68
81
  * ```
69
82
  *
70
- * A request Vite answers gets an empty `HttpServerResponse`, which Effect's
71
- * Node server does not write, since Vite has ended the Node response. A Vite
72
- * error fails the request as a defect.
83
+ * The layer registers one catch-all route on the server's router. Its
84
+ * handler passes each request to Vite first. For a request that Vite passes
85
+ * on, it calls `loadApp`. When `loadApp` returns a new `App` value, such as
86
+ * after an edit of the app's server code, the layer builds the new `App` with
87
+ * its own router and closes the old one. Concurrent requests share one build
88
+ * of each `App`. Then the request goes through the current `App`.
89
+ *
90
+ * A request that Vite answers gets an empty `HttpServerResponse`, which
91
+ * Effect's Node server does not write, since Vite has ended the Node
92
+ * response. A defect, such as a Vite error, a `loadApp` that rejects or an
93
+ * `App` that fails to build, goes to the app's loggers, after
94
+ * `ssrFixStacktrace` maps its stack, and the client gets an empty 500.
95
+ *
96
+ * @stability unstable
73
97
  */
74
- export const devMiddleware =
75
- (vite: ViteDevServer) =>
76
- <E, R>(
77
- app: Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>,
78
- ): Effect.Effect<
79
- HttpServerResponse.HttpServerResponse,
80
- E,
81
- R | HttpServerRequest.HttpServerRequest
82
- > =>
83
- HttpServerRequest.HttpServerRequest.use((request) =>
84
- passThroughVite(
85
- vite,
86
- // Vite takes HTTP/1 requests; Effect's Node server serves HTTP/1.
87
- NodeHttpServerRequest.toIncomingMessage(request) as Http.IncomingMessage,
88
- NodeHttpServerRequest.toServerResponse(request) as Http.ServerResponse,
89
- ).pipe(
90
- Effect.flatMap((passed) => (passed ? app : Effect.succeed(HttpServerResponse.empty()))),
91
- ),
92
- );
98
+ export const layer = <E, R>(
99
+ vite: ViteDevServer,
100
+ loadApp: () => PromiseLike<Layer.Layer<never, E, R>>,
101
+ ) =>
102
+ HttpRouter.use((router) =>
103
+ Effect.gen(function* () {
104
+ const layerScope = yield* Effect.scope;
105
+ const services =
106
+ yield* Effect.context<Exclude<HttpRouter.Request.Without<R>, HttpRouter.HttpRouter>>();
107
+ /** Builds `app` in `scope`. The services of this layer do not replace either. */
108
+ const build = (app: Layer.Layer<never, E, R>, scope: Scope.Scope) =>
109
+ HttpRouter.toHttpEffect(app).pipe(
110
+ Scope.provide(scope),
111
+ // A new memo map, so the App gets its own router. The memo map of the
112
+ // server's layers has the server's router, and a fork of it reuses it.
113
+ Effect.provideServiceEffect(Layer.CurrentMemoMap, Layer.makeMemoMap),
114
+ Effect.provideContext(services),
115
+ );
116
+ type Handler = Effect.Success<ReturnType<typeof build>>;
117
+ interface Built {
118
+ readonly app: Layer.Layer<never, E, R>;
119
+ readonly handler: Handler;
120
+ readonly scope: Scope.Closeable;
121
+ }
122
+ const state = yield* SynchronizedRef.make(Option.none<Built>());
123
+
124
+ /** The handler of `app`, built once for each `App` value. */
125
+ const handlerOf = (app: Layer.Layer<never, E, R>) =>
126
+ SynchronizedRef.modifyEffect(state, (previous) =>
127
+ Effect.gen(function* () {
128
+ if (Option.isSome(previous) && previous.value.app === app) {
129
+ return Tuple.make(previous.value.handler, previous);
130
+ }
131
+ const scope = yield* Scope.fork(layerScope);
132
+ const handler = yield* build(app, scope).pipe(
133
+ Effect.onExit((exit) =>
134
+ Exit.isSuccess(exit) ? Effect.void : Scope.close(scope, exit),
135
+ ),
136
+ Effect.orDie,
137
+ );
138
+ if (Option.isSome(previous)) yield* Scope.close(previous.value.scope, Exit.void);
139
+ return Tuple.make(handler, Option.some<Built>({ app, handler, scope }));
140
+ }).pipe(
141
+ // A client that leaves during a build does not stop it: the build
142
+ // is shared, and a stop could leave a scope open or drop a build.
143
+ Effect.uninterruptible,
144
+ ),
145
+ );
146
+
147
+ const handler = HttpServerRequest.HttpServerRequest.use((request) =>
148
+ passThroughVite(
149
+ vite,
150
+ // Vite takes HTTP/1 requests; Effect's Node server serves HTTP/1.
151
+ NodeHttpServerRequest.toIncomingMessage(request) as Http.IncomingMessage,
152
+ NodeHttpServerRequest.toServerResponse(request) as Http.ServerResponse,
153
+ ).pipe(
154
+ Effect.flatMap((passed) =>
155
+ passed
156
+ ? Effect.promise(loadApp).pipe(Effect.flatMap(handlerOf), Effect.flatten)
157
+ : Effect.succeed(HttpServerResponse.empty()),
158
+ ),
159
+ Effect.catchDefect((defect) => {
160
+ if (Predicate.isError(defect)) vite.ssrFixStacktrace?.(defect);
161
+ return Effect.logError("Request failed in development", defect).pipe(
162
+ Effect.as(HttpServerResponse.empty({ status: 500 })),
163
+ );
164
+ }),
165
+ ),
166
+ );
167
+ yield* router.add("*", "*", handler);
168
+ }),
169
+ );