react-router-effect 2.0.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 ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Runs React Router's development server, Vite, in front of the Effect HTTP
3
+ * app on Node.
4
+ *
5
+ * In framework mode React Router builds with Vite, and in development Vite
6
+ * serves the client: route modules, `/@vite/client`, the files of `public/`,
7
+ * and its error overlay. A custom server runs Vite in middleware mode and passes
8
+ * it each request first. Vite's middleware is a Connect stack of Node
9
+ * requests and responses, so this layer needs Effect's Node HTTP server.
10
+ *
11
+ * Its API is unstable: a minor version can change it.
12
+ *
13
+ * @stability unstable
14
+ * @since 2.1.0
15
+ */
16
+ import type * as Http from "node:http";
17
+
18
+ import * as NodeHttpServerRequest from "@effect/platform-node/NodeHttpServerRequest";
19
+ import { Effect, Exit, Layer, Option, Predicate, Scope, SynchronizedRef, Tuple } from "effect";
20
+ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
21
+
22
+ /**
23
+ * The parts of a Vite dev server that the adapter uses.
24
+ *
25
+ * @stability unstable
26
+ */
27
+ export interface ViteDevServer {
28
+ /** Vite's Connect middleware stack. */
29
+ readonly middlewares: (
30
+ request: Http.IncomingMessage,
31
+ response: Http.ServerResponse,
32
+ next: (error?: unknown) => void,
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;
39
+ }
40
+
41
+ /**
42
+ * Runs Vite's middleware on the Node request. Succeeds with `true` when Vite
43
+ * passes the request on, and `false` when Vite answered it, or the client left
44
+ * while Vite answered.
45
+ */
46
+ const passThroughVite = (
47
+ vite: ViteDevServer,
48
+ request: Http.IncomingMessage,
49
+ response: Http.ServerResponse,
50
+ ): Effect.Effect<boolean> =>
51
+ Effect.callback<boolean>((resume) => {
52
+ let settled = false;
53
+ const settle = (result: Effect.Effect<boolean>) => {
54
+ if (settled) return;
55
+ settled = true;
56
+ response.off("finish", answered);
57
+ response.off("close", answered);
58
+ resume(result);
59
+ };
60
+ const answered = () => settle(Effect.succeed(false));
61
+ response.once("finish", answered);
62
+ response.once("close", answered);
63
+ try {
64
+ vite.middlewares(request, response, (error) =>
65
+ settle(error === undefined ? Effect.succeed(true) : Effect.die(error)),
66
+ );
67
+ } catch (error) {
68
+ settle(Effect.die(error));
69
+ }
70
+ });
71
+
72
+ /**
73
+ * Serves the app in development, as the Express template does. Give it to
74
+ * `HttpRouter.serve` in place of the app's `App`:
75
+ *
76
+ * ```js
77
+ * const vite = await createServer({ server: { middlewareMode: true } });
78
+ * const App = ReactRouterVite.layer(vite, () =>
79
+ * vite.ssrLoadModule("./server/app.ts").then((module) => module.App),
80
+ * );
81
+ * ```
82
+ *
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
97
+ */
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
+ );