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/README.md +724 -171
- package/dist/index.d.mts +2 -342
- package/dist/index.mjs +122 -33
- package/dist/rolldown-runtime-D8jQpDG7.mjs +12 -0
- package/dist/route-data-CWBxnNAA.d.mts +549 -0
- package/dist/server.d.mts +284 -0
- package/dist/server.mjs +483 -0
- package/dist/vite.d.mts +48 -0
- package/dist/vite.mjs +80 -0
- package/package.json +25 -9
- package/src/action-meta.ts +58 -0
- package/src/assets.ts +212 -0
- package/src/check.ts +213 -0
- package/src/errors.ts +3 -3
- package/src/factory.ts +301 -91
- package/src/index.ts +22 -1
- package/src/route-data.ts +151 -0
- package/src/server-entry.ts +11 -0
- package/src/server.ts +659 -0
- package/src/vite-entry.ts +2 -0
- package/src/vite.ts +169 -0
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
|
+
);
|