react-router-effect 2.0.0 → 2.1.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.
@@ -0,0 +1,218 @@
1
+ import { C as ReturnableDataError, _ as NameError, a as RegisteredMeta, g as LibraryHandled, h as IsAny, m as FailureResponse, o as RegisteredRecover, p as FactoryTypes, y as RouteHandlers } from "./route-data-MgSnnR33.mjs";
2
+ import { Cause, Context, Effect, FileSystem, Path } from "effect";
3
+ import { Register, ServerBuild } from "react-router";
4
+ import { HttpPlatform, HttpServerError, HttpServerRequest, HttpServerResponse } from "effect/http";
5
+ declare namespace server_d_exports {
6
+ export { App, BuildSource, ClientAssetServices, ClientBuildError, Options, make };
7
+ }
8
+ /** A React Router server build, or a function that loads it, as in `createRequestHandler`. */
9
+ type BuildSource = ServerBuild | (() => ServerBuild | Promise<ServerBuild>);
10
+ /** The options of {@link make}. */
11
+ interface Options {
12
+ /** The server build: `() => import("virtual:react-router/server-build")`. */
13
+ readonly build: BuildSource;
14
+ /** The route factory whose runner, runtime and error model route Effects use. */
15
+ readonly factory: {
16
+ readonly handlers: RouteHandlers;
17
+ };
18
+ /** React Router's server mode. Defaults to `process.env.NODE_ENV`. */
19
+ readonly mode?: string | undefined;
20
+ /**
21
+ * Serves the client build before React Router: the files of the build's
22
+ * `assetsBuildDirectory`, under its `publicPath`. Files under `assets/` have
23
+ * hashed names and are cached for a year as immutable, other files for an
24
+ * hour. It serves like `express.static`: a directory serves its index file,
25
+ * such as a prerendered page, its path without the trailing slash redirects
26
+ * to the path with it, dotfiles are not served, and a request it does not
27
+ * serve goes to React Router. Like `express.static`, it looks each GET and
28
+ * HEAD request up in the file system. It serves only a build that it loads
29
+ * once: in production mode, or a build value. A
30
+ * development build is never served, since Vite serves the client there, nor
31
+ * is a `publicPath` on another origin. Making the app fails with a
32
+ * {@link ClientBuildError} when it cannot read the directory. Needs the
33
+ * platform's file system, which `NodeHttpServer.layer` and the other server
34
+ * layers provide.
35
+ */
36
+ readonly clientAssets?: boolean | undefined;
37
+ /**
38
+ * Builds the services of one request, before React Router runs. They join
39
+ * the request's effect context, so every middleware, loader and action sees
40
+ * them without a middleware that sets them.
41
+ */
42
+ readonly requestContext?: ((request: Request) => Context.Context<never>) | undefined;
43
+ }
44
+ /** The services that serving the client build needs. */
45
+ type ClientAssetServices = FileSystem.FileSystem | Path.Path | HttpPlatform.HttpPlatform;
46
+ declare const ClientBuildError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
47
+ readonly _tag: "ClientBuildError";
48
+ } & Readonly<A>;
49
+ /**
50
+ * The app serves the client build, and cannot read its directory. The
51
+ * build's `assetsBuildDirectory` is relative to the working directory of the
52
+ * server, so `directory` is the path it resolved to.
53
+ */
54
+ declare class ClientBuildError extends ClientBuildError_base<{
55
+ readonly message: string;
56
+ /** The resolved directory of the client build. */
57
+ readonly directory: string;
58
+ readonly cause: unknown;
59
+ }> {}
60
+ /** True when the options serve the client build. */
61
+ type ServesClientAssets<O> = O extends {
62
+ readonly clientAssets: infer Serves;
63
+ } ? [Serves] extends [false | undefined] ? false : true : false;
64
+ /**
65
+ * The Effect HTTP app {@link make} makes. Serving the client build adds the
66
+ * static file server's errors to its errors.
67
+ */
68
+ type App<O extends Options> = Effect.Effect<HttpServerResponse.HttpServerResponse, HttpServerError.RequestError | (ServesClientAssets<O> extends true ? HttpServerError.HttpServerError : never), HttpServerRequest.HttpServerRequest>;
69
+ /**
70
+ * Makes the Effect HTTP app that serves React Router. Run the Effect once,
71
+ * when the server starts. In production it loads the build, and with
72
+ * `clientAssets` it reads the client build's directory, so a missing one fails
73
+ * the server's start with a {@link ClientBuildError} and not its requests.
74
+ *
75
+ * ```ts
76
+ * import { ReactRouterHttp } from "react-router-effect/server";
77
+ *
78
+ * export const reactRouterApp = ReactRouterHttp.make({
79
+ * build: () => import("virtual:react-router/server-build"),
80
+ * factory: routeFactory,
81
+ * clientAssets: true,
82
+ * });
83
+ *
84
+ * // Serve it like any Effect HTTP app, with the app's own middleware.
85
+ * Layer.unwrap(
86
+ * Effect.map(reactRouterApp, (app) => HttpServer.serve(app, HttpMiddleware.logger)),
87
+ * );
88
+ * ```
89
+ *
90
+ * In development, where the build changes, the app loads the build for each
91
+ * request, and making it does nothing else.
92
+ *
93
+ * The request signal aborts only when the response ends early: when the
94
+ * client leaves while React Router runs or while the body streams. A
95
+ * completed response never aborts it.
96
+ */
97
+ declare const make: <const O extends Options>(options: O) => Effect.Effect<App<O>, ServesClientAssets<O> extends true ? ClientBuildError : never, ServesClientAssets<O> extends true ? ClientAssetServices : never>;
98
+ /** The route modules React Router's typegen registers, keyed by route ID. */
99
+ type RegisteredRouteModules = Register extends {
100
+ readonly routeModules: infer Modules;
101
+ } ? Modules : {};
102
+ /** What an effect middleware may succeed with: React Router's response, or nothing. */
103
+ type MiddlewareResult = Response | void | undefined;
104
+ /** What a failure says to do, under its `react-router-effect` key. */
105
+ type FailureMessage = "This route Effect can fail with an error the route factory does not handle, or needs a service it does not provide. Catch the error in the Effect (for example with Effect.catchTag), add it to the factory's domain errors, or provide the service.";
106
+ /**
107
+ * One export that fails the check. `unhandledErrors` lists each error the
108
+ * effect can fail with that the factory does not handle, by its `_tag`.
109
+ * `missingServices` lists each service that neither the runtime nor the
110
+ * request context provides. `invalidResults` lists what a middleware succeeds
111
+ * with that is not a `Response` or nothing.
112
+ *
113
+ * It is an inline object type, not an interface, so the compiler prints its
114
+ * fields in the error rather than an alias name.
115
+ */
116
+ type RouteEffectFailure<Route, Export, Unhandled, Missing, Invalid> = {
117
+ readonly "react-router-effect": FailureMessage;
118
+ readonly route: Route;
119
+ readonly export: Export;
120
+ readonly unhandledErrors: Unhandled;
121
+ readonly missingServices: Missing;
122
+ readonly invalidResults: Invalid;
123
+ };
124
+ /**
125
+ * The failure of one route function, or `never` when it does not return an
126
+ * Effect or when its Effect is fully handled. An Effect whose error or
127
+ * service type is `any` comes from a mistake TypeScript already reports, so it
128
+ * is skipped rather than reported a second time.
129
+ */
130
+ type FunctionFailure<Route, Export, F, Handled, Provided, Result> = F extends ((...args: never[]) => Effect.Effect<infer A, infer E, infer R>) ? true extends IsAny<E> | IsAny<R> ? never : [Exclude<E, Handled>, Exclude<R, Provided>, Exclude<A, Result>] extends [never, never, never] ? never : {
131
+ readonly "react-router-effect": FailureMessage;
132
+ readonly route: Route;
133
+ readonly export: Export;
134
+ readonly unhandledErrors: NameError<Exclude<E, Handled>>;
135
+ readonly missingServices: Exclude<R, Provided>;
136
+ readonly invalidResults: Exclude<A, Result>;
137
+ } : never;
138
+ /** The failures of one route module's loader, action and middleware. */
139
+ type ModuleFailures<Route, Module, Handled, Provided> = (Module extends {
140
+ readonly loader: infer F;
141
+ } ? FunctionFailure<Route, "loader", F, Handled, Provided, unknown> : never) | (Module extends {
142
+ readonly action: infer F;
143
+ } ? FunctionFailure<Route, "action", F, Handled, Provided, unknown> : never) | (Module extends {
144
+ readonly middleware: ReadonlyArray<infer F>;
145
+ } ? FunctionFailure<Route, "middleware", F, Handled | FailureResponse, Provided, MiddlewareResult> : never);
146
+ /** The failures of every registered route module. */
147
+ type AllFailures<Modules, Handled, Provided> = { [Route in keyof Modules]: ModuleFailures<Route, Modules[Route], Handled, Provided>; }[keyof Modules];
148
+ /** The types of a route factory. */
149
+ type TypesOf<Factory> = Factory extends {
150
+ readonly "~types": infer Types;
151
+ } ? Types : never;
152
+ /** What one error handler's return recovers into, or `never` when it throws. */
153
+ type HandlerRecovered<Return> = Return extends ReturnableDataError<infer Body> ? Body : Return extends Effect.Effect<infer Success, infer _E, infer _R> ? Success : never;
154
+ /** The factory's recover map: each error tag whose handler recovers, and its data. */
155
+ type FactoryRecover<Returns> = { [Tag in keyof Returns as [HandlerRecovered<Returns[Tag]>] extends [never] ? never : Tag]: HandlerRecovered<Returns[Tag]>; };
156
+ /** True when each type is assignable to the other. */
157
+ type SameType<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
158
+ /**
159
+ * A disagreement between the `Register` of `react-router-effect` and the
160
+ * factory, which would type plain Effect routes' data wrongly.
161
+ */
162
+ interface RegisterMismatch<Key, Factory, Registered> {
163
+ readonly register: Key;
164
+ readonly factory: Factory;
165
+ readonly registered: Registered;
166
+ }
167
+ /** The registration mismatches of a factory's types. */
168
+ type RegisterMismatches<Returns, Meta> = (SameType<FactoryRecover<Returns>, RegisteredRecover> extends true ? never : RegisterMismatch<"recover", FactoryRecover<Returns>, RegisteredRecover>) | (SameType<Meta, RegisteredMeta> extends true ? never : RegisterMismatch<"actionMeta", Meta, RegisteredMeta>);
169
+ /** Every failure: the routes', then the registration's. */
170
+ type Failures<Modules, Types> = Types extends FactoryTypes<infer DomainError, infer Returns, infer Services, infer Meta> ? AllFailures<Modules, LibraryHandled<DomainError>, Services> | RegisterMismatches<Returns, Meta> : never;
171
+ /**
172
+ * The failures of every Effect route function across the route modules React
173
+ * Router's typegen registers, and the registration mismatches, as objects.
174
+ * `never` when everything is handled. `RouteEffectCheck` reports them.
175
+ */
176
+ type RouteEffectFailures<Factory, Modules = RegisteredRouteModules> = Failures<Modules, TypesOf<Factory>>;
177
+ /** A printable name for an error tag or another unhandled error type. */
178
+ type ErrorName<Name> = Name extends string ? Name : "an untagged error";
179
+ /** A printable name for a service: its key, such as `"app/Database"`. */
180
+ type ServiceName<Service> = Service extends {
181
+ readonly key: infer Key extends string;
182
+ } ? Key : "a service without a string key";
183
+ /** The diagnostic lines of one failure, one per error, service or result. */
184
+ type FailureLines<Failure> = Failure extends {
185
+ readonly route: infer Route extends string | number | symbol;
186
+ readonly export: infer Export extends string;
187
+ readonly unhandledErrors: infer Unhandled;
188
+ readonly missingServices: infer Missing;
189
+ readonly invalidResults: infer Invalid;
190
+ } ? (Unhandled extends unknown ? `${Route & string}#${Export} must handle ${ErrorName<Unhandled>}` : never) | (Missing extends unknown ? `${Route & string}#${Export} needs a provider for ${ServiceName<Missing>}` : never) | ([Invalid] extends [never] ? never : `${Route & string}#${Export} must succeed with a Response or nothing`) : Failure extends {
191
+ readonly register: infer Key extends string;
192
+ } ? `the Register of react-router-effect: ${Key} does not match the route factory` : never;
193
+ /**
194
+ * Checks every loader, action and middleware that returns an Effect, across
195
+ * the route modules React Router's typegen registers. Each failure is a
196
+ * required property whose name says what to fix, so assigning `{}` fails to
197
+ * compile with a list such as:
198
+ *
199
+ * ```text
200
+ * Type '{}' is missing the following properties from type ...:
201
+ * "routes/billing#loader must handle PlaidApiError",
202
+ * "routes/admin#middleware needs a provider for app/Session"
203
+ * ```
204
+ *
205
+ * ```ts
206
+ * import type { RouteEffectCheck } from "react-router-effect/server";
207
+ *
208
+ * export const routeEffects: RouteEffectCheck<typeof routeFactory> = {};
209
+ * ```
210
+ *
211
+ * An Effect may fail with the factory's domain errors, the library's `Respond`
212
+ * errors, and `HttpServerRespondable` errors, and require the services of the
213
+ * factory's runtime and request context. A middleware may also fail with what
214
+ * its `next()` fails with. The check also fails when the `Register` of
215
+ * `react-router-effect` disagrees with the factory.
216
+ */
217
+ type RouteEffectCheck<Factory, Modules = RegisteredRouteModules> = { readonly [Line in FailureLines<RouteEffectFailures<Factory, Modules>>]: Line; };
218
+ export { server_d_exports as ReactRouterHttp, type RegisterMismatch, type RouteEffectCheck, type RouteEffectFailure, type RouteEffectFailures };
@@ -0,0 +1,337 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.mjs";
2
+ import { Cause, Context, Data, Effect, Exit, FileSystem, Option, Path, Scope, Stream, Tracer } from "effect";
3
+ import { RouterContextProvider, createContext, createRequestHandler } from "react-router";
4
+ import { Cookies, HttpServerRequest, HttpServerResponse, HttpStaticServer } from "effect/http";
5
+ // src/server.ts
6
+ /**
7
+ * Serves a React Router framework-mode build from Effect's Node HTTP server.
8
+ *
9
+ * The adapter converts each `HttpServerRequest` into a Web `Request`, calls
10
+ * React Router's own request handler, and streams the `Response` back. React
11
+ * Router keeps doing everything it does for any other server adapter:
12
+ * documents, `.data` requests, streamed promises, resource routes, the lazy
13
+ * route manifest, and middleware.
14
+ *
15
+ * The adapter owns everything between the Effect HTTP request and React
16
+ * Router: the Web `Request` and its abort signal, the route factory, and the
17
+ * conversion of React Router's `Response`, cookies and streamed body included.
18
+ * Listening, static files, compression, proxies, tracing and the other server
19
+ * concerns stay with the app's Effect HTTP server.
20
+ *
21
+ * Before React Router sees the build, the adapter wraps every loader, action
22
+ * and middleware with the route factory's runner. A route function that
23
+ * returns an Effect runs through the factory's error model, with the factory's
24
+ * runtime and the request's effect context. Any other route function passes
25
+ * through unchanged, so existing promise loaders and `makeLoader` wrappers keep
26
+ * working.
27
+ *
28
+ * @since 2.1.0
29
+ */
30
+ var server_exports = /* @__PURE__ */ __exportAll({
31
+ ClientBuildError: () => ClientBuildError,
32
+ make: () => make
33
+ });
34
+ /**
35
+ * Converts React Router's response. Its `Set-Cookie` values become the
36
+ * response's cookies, as `HttpServerResponse.fromWeb` converts them, so
37
+ * middleware after the adapter can read and change them. Effect keeps one
38
+ * cookie per name, the last one, and only the attributes it knows.
39
+ *
40
+ * Effect gives a streamed body without a content type
41
+ * `application/octet-stream`, so a route that sends a body sets its own.
42
+ *
43
+ * When the body stream ends before it finished, because the client left or a
44
+ * write failed, the request signal aborts: React Router then stops streamed
45
+ * promises, and route code sees the abort. A completed response never aborts it.
46
+ */
47
+ const toHttpResponse = (response, controller) => {
48
+ const headers = new Headers(response.headers);
49
+ headers.delete("set-cookie");
50
+ const init = {
51
+ status: response.status,
52
+ statusText: response.statusText === "" ? void 0 : response.statusText,
53
+ headers,
54
+ cookies: Cookies.fromSetCookie(response.headers.getSetCookie())
55
+ };
56
+ const body = response.body;
57
+ return body === null ? HttpServerResponse.empty(init) : HttpServerResponse.stream(Stream.fromReadableStream({
58
+ evaluate: () => body,
59
+ onError: (error) => error
60
+ }).pipe(Stream.onExit((exit) => Exit.isSuccess(exit) ? Effect.void : Effect.sync(() => controller.abort()))), init);
61
+ };
62
+ /** The `Cache-Control` of the files under `assets/`, whose names include a content hash. */
63
+ const hashedCacheControl = "public, max-age=31536000, immutable";
64
+ /** The `Cache-Control` of the other client files, such as `favicon.ico`. */
65
+ const otherCacheControl = "public, max-age=3600";
66
+ /**
67
+ * True for a path with a segment that starts with a dot, such as `.vite/`,
68
+ * which is never served. Effect's static server serves dotfiles, and Vite
69
+ * writes its manifest there. `express.static` ignores them.
70
+ */
71
+ const hasDotSegment = (path) => path.split("/").some((segment) => segment.startsWith("."));
72
+ /** True when a static lookup found no file, so the request goes on to React Router. */
73
+ const isNotFound = (error) => error.reason._tag === "RouteNotFound";
74
+ /**
75
+ * The app serves the client build, and cannot read its directory. The
76
+ * build's `assetsBuildDirectory` is relative to the working directory of the
77
+ * server, so `directory` is the path it resolved to.
78
+ */
79
+ var ClientBuildError = class extends Data.TaggedError("ClientBuildError") {};
80
+ /**
81
+ * The client files of a build, or `undefined` when the adapter does not serve
82
+ * them. Making them reads the build's directory, so a missing client build
83
+ * fails the server's start and not its requests.
84
+ */
85
+ const makeClientFiles = (build) => Effect.gen(function* () {
86
+ if (build.assets.hmr !== void 0 || !build.publicPath.startsWith("/")) return void 0;
87
+ const fileSystem = yield* FileSystem.FileSystem;
88
+ const root = (yield* Path.Path).resolve(build.assetsBuildDirectory);
89
+ yield* fileSystem.readDirectory(root).pipe(Effect.mapError((cause) => new ClientBuildError({
90
+ message: `Cannot read the client build at ${root}`,
91
+ directory: root,
92
+ cause
93
+ })));
94
+ const files = (cacheControl) => HttpStaticServer.make({
95
+ root,
96
+ cacheControl
97
+ }).pipe(Effect.orDie);
98
+ return {
99
+ prefix: build.publicPath.replace(/\/$/, ""),
100
+ hashed: yield* files(hashedCacheControl),
101
+ other: yield* files(otherCacheControl)
102
+ };
103
+ });
104
+ /** The decoded path of a URL, or `undefined` when it does not decode. */
105
+ const decodePath = (path) => {
106
+ try {
107
+ return decodeURIComponent(path);
108
+ } catch {
109
+ return;
110
+ }
111
+ };
112
+ /**
113
+ * Serves the request from the client build, or returns `None` when the build
114
+ * has no such file: a path the build does not have, a directory without an
115
+ * index file, a dotfile, a path outside the `publicPath`, or a method other
116
+ * than GET and HEAD.
117
+ */
118
+ const serveClientFile = (files, request) => {
119
+ const path = request.url.split("?")[0] ?? "";
120
+ if (request.method !== "GET" && request.method !== "HEAD" || !path.startsWith(`${files.prefix}/`)) return Effect.succeedNone;
121
+ const url = request.url.slice(files.prefix.length);
122
+ const file = decodePath(url.split("?")[0] ?? "");
123
+ if (file === void 0 || hasDotSegment(file)) return Effect.succeedNone;
124
+ return (url.startsWith("/assets/") ? files.hashed : files.other).pipe(Effect.provideService(HttpServerRequest.HttpServerRequest, request.modify({ url })), Effect.map(Option.some), Effect.catchIf(isNotFound, () => Effect.succeedNone));
125
+ };
126
+ /**
127
+ * The span of a traced request: the server span of Effect's HTTP tracer, with
128
+ * a tracer that exports spans. Without it, the adapter adds no spans.
129
+ */
130
+ const requestSpanKey = createContext(void 0);
131
+ /**
132
+ * The span of the request fiber, when a tracer exports it: not the native
133
+ * tracer, whose spans go nowhere, and not an unsampled span.
134
+ */
135
+ const exportedSpan = (context) => {
136
+ const span = Context.getOrUndefined(context, Tracer.ParentSpan);
137
+ return span?._tag === "Span" && span.sampled && Context.get(context, Tracer.Tracer) !== Tracer.nativeTracer ? span : void 0;
138
+ };
139
+ /** Calls a route function and captures its result, a synchronous throw included. */
140
+ const settle = (call) => {
141
+ try {
142
+ return Promise.resolve(call()).then((value) => ({
143
+ ok: true,
144
+ value
145
+ }), (error) => ({
146
+ ok: false,
147
+ error
148
+ }));
149
+ } catch (error) {
150
+ return Promise.resolve({
151
+ ok: false,
152
+ error
153
+ });
154
+ }
155
+ };
156
+ /** The status of a thrown `Response` or `data(...)`, which React Router sends as a response. */
157
+ const thrownStatus = (error) => {
158
+ if (error instanceof Response) return error.status;
159
+ if (typeof error !== "object" || error === null || !("type" in error)) return void 0;
160
+ if (error.type !== "DataWithResponseInit" || !("init" in error)) return void 0;
161
+ const init = error.init;
162
+ return typeof init === "object" && init !== null && "status" in init && typeof init.status === "number" ? init.status : 200;
163
+ };
164
+ /**
165
+ * A thrown value is an error of the span unless React Router sends it as a
166
+ * response below 500, such as a redirect or `data(..., 404)`.
167
+ */
168
+ const isSpanError = (error) => {
169
+ const status = thrownStatus(error);
170
+ return status === void 0 || status >= 500;
171
+ };
172
+ /**
173
+ * Runs a route function in a span of its own when the request is traced,
174
+ * such as `react-router.loader routes/home`, and sets `http.route` on the
175
+ * request's span.
176
+ *
177
+ * The spans follow React Router's structure. A middleware's span is the
178
+ * parent of everything its `next()` runs: the next middleware, then the
179
+ * loaders and the action. A route Effect nests under its function's span, and
180
+ * so do the clients an OpenTelemetry instrumentation traces, since Effect's
181
+ * OpenTelemetry tracer makes the span active while the function is called.
182
+ *
183
+ * The request store's parent span passes the structure down. A loader or an
184
+ * action reads it once, when it is called. A middleware's span stays there
185
+ * for the rest of the chain, until the middleware finishes; an Effect
186
+ * middleware's own spans around `next()` take its place there.
187
+ */
188
+ const traced = (operation, routeId, storeKey, fn) => (args, ...rest) => {
189
+ const requestSpan = args.context.get(requestSpanKey);
190
+ if (requestSpan === void 0) return fn(args, ...rest);
191
+ if (typeof args.pattern === "string") {
192
+ const route = args.pattern.startsWith("/") ? args.pattern : `/${args.pattern}`;
193
+ requestSpan.attribute("http.route", route);
194
+ }
195
+ const store = args.context.get(storeKey);
196
+ const program = Effect.useSpan(`react-router.${operation} ${routeId}`, {
197
+ kind: "internal",
198
+ attributes: {
199
+ "react_router.operation.name": operation,
200
+ "react_router.route.id": routeId
201
+ }
202
+ }, (span) => Effect.sync(() => {
203
+ args.context.set(storeKey, Context.add(store, Tracer.ParentSpan, span));
204
+ const settled = settle(() => fn(args, ...rest));
205
+ if (operation !== "middleware") args.context.set(storeKey, store);
206
+ return settled;
207
+ }).pipe(Effect.withParentSpan(span), Effect.flatMap((settled) => Effect.promise(() => settled)), Effect.flatMap((result) => result.ok || !isSpanError(result.error) ? Effect.succeed(result) : Effect.fail(result.error))));
208
+ return Effect.runPromiseExitWith(store)(program).then((exit) => {
209
+ if (operation === "middleware") args.context.set(storeKey, store);
210
+ if (Exit.isFailure(exit)) throw Cause.squash(exit.cause);
211
+ if (exit.value.ok) return exit.value.value;
212
+ throw exit.value.error;
213
+ });
214
+ };
215
+ /**
216
+ * Wraps a route module's loader, action and middleware with the factory's
217
+ * runner, and traces them. The module itself is left as it is.
218
+ */
219
+ const wrapModule = (routeId, module, handlers) => {
220
+ const store = handlers.requestContext;
221
+ return {
222
+ ...module,
223
+ loader: module.loader === void 0 ? void 0 : traced("loader", routeId, store, handlers.loader(module.loader)),
224
+ action: module.action === void 0 ? void 0 : traced("action", routeId, store, handlers.action(module.action)),
225
+ middleware: module.middleware?.map((fn) => traced("middleware", routeId, store, handlers.middleware(fn)))
226
+ };
227
+ };
228
+ /** Wraps every route module of a build. */
229
+ const wrapBuild = (build, handlers) => ({
230
+ ...build,
231
+ routes: Object.fromEntries(Object.entries(build.routes).map(([id, route]) => [id, route === void 0 ? void 0 : {
232
+ ...route,
233
+ module: wrapModule(id, route.module, handlers)
234
+ }]))
235
+ });
236
+ /**
237
+ * Loads the build and returns React Router's request handler for it. The
238
+ * handler is built once per build value: React Router rebuilds its route
239
+ * tables on every request when it is given a function, so the adapter
240
+ * resolves the build itself. In development a new build after a change gets a
241
+ * new handler. The last handler is kept across runs of {@link make}'s Effect,
242
+ * since a development server can make the app for each request.
243
+ */
244
+ const makeBuildLoader = (options, mode) => {
245
+ let cached;
246
+ return Effect.promise(async () => {
247
+ const build = typeof options.build === "function" ? await options.build() : options.build;
248
+ if (cached?.build !== build) cached = {
249
+ build,
250
+ handler: createRequestHandler(wrapBuild(build, options.factory.handlers), mode)
251
+ };
252
+ return cached;
253
+ });
254
+ };
255
+ /**
256
+ * Calls React Router. The call starts in the request fiber, so code that
257
+ * reads the async context, such as an OpenTelemetry tracer whose context the
258
+ * Effect tracer sets, sees the request's. When the request fiber is
259
+ * interrupted, because the client left, the request signal aborts, and a
260
+ * response that React Router resolves afterwards has its body cancelled.
261
+ *
262
+ * Effect's HTTP server runs the app uninterruptible, and interrupts the fiber
263
+ * only where the app opts in, so the call is interruptible.
264
+ */
265
+ const callRouter = (handler, request, context, controller) => Effect.callback((resume) => {
266
+ let interrupted = false;
267
+ handler(request, context).then((response) => {
268
+ if (interrupted) {
269
+ response.body?.cancel().catch(() => void 0);
270
+ return;
271
+ }
272
+ resume(Effect.succeed(response));
273
+ }, (error) => {
274
+ if (!interrupted) resume(Effect.die(error));
275
+ });
276
+ return Effect.sync(() => {
277
+ interrupted = true;
278
+ controller.abort();
279
+ });
280
+ }).pipe(Effect.interruptible);
281
+ /**
282
+ * Makes the Effect HTTP app that serves React Router. Run the Effect once,
283
+ * when the server starts. In production it loads the build, and with
284
+ * `clientAssets` it reads the client build's directory, so a missing one fails
285
+ * the server's start with a {@link ClientBuildError} and not its requests.
286
+ *
287
+ * ```ts
288
+ * import { ReactRouterHttp } from "react-router-effect/server";
289
+ *
290
+ * export const reactRouterApp = ReactRouterHttp.make({
291
+ * build: () => import("virtual:react-router/server-build"),
292
+ * factory: routeFactory,
293
+ * clientAssets: true,
294
+ * });
295
+ *
296
+ * // Serve it like any Effect HTTP app, with the app's own middleware.
297
+ * Layer.unwrap(
298
+ * Effect.map(reactRouterApp, (app) => HttpServer.serve(app, HttpMiddleware.logger)),
299
+ * );
300
+ * ```
301
+ *
302
+ * In development, where the build changes, the app loads the build for each
303
+ * request, and making it does nothing else.
304
+ *
305
+ * The request signal aborts only when the response ends early: when the
306
+ * client leaves while React Router runs or while the body streams. A
307
+ * completed response never aborts it.
308
+ */
309
+ const make = (options) => {
310
+ const mode = options.mode ?? process.env.NODE_ENV;
311
+ const loadBuild = makeBuildLoader(options, mode);
312
+ const settled = typeof options.build !== "function" || mode === "production";
313
+ return Effect.gen(function* () {
314
+ if (!settled) return serve(options, loadBuild, void 0);
315
+ const loaded = yield* loadBuild;
316
+ const files = options.clientAssets === true ? yield* makeClientFiles(loaded.build) : void 0;
317
+ return serve(options, Effect.succeed(loaded), files);
318
+ });
319
+ };
320
+ /** The app for one loaded build source, and the client files it serves, if any. */
321
+ const serve = (options, loadBuild, files) => Effect.gen(function* () {
322
+ const httpRequest = yield* HttpServerRequest.HttpServerRequest;
323
+ if (files !== void 0) {
324
+ const file = yield* serveClientFile(files, httpRequest);
325
+ if (Option.isSome(file)) return file.value;
326
+ }
327
+ const { handler } = yield* loadBuild;
328
+ const fiberContext = Context.omit(Scope.Scope)(yield* Effect.context());
329
+ const controller = new AbortController();
330
+ const request = yield* HttpServerRequest.toWeb(httpRequest, { signal: controller.signal });
331
+ const routerContext = new RouterContextProvider();
332
+ routerContext.set(options.factory.handlers.requestContext, Context.merge(fiberContext, options.requestContext?.(request) ?? Context.empty()));
333
+ routerContext.set(requestSpanKey, exportedSpan(fiberContext));
334
+ const response = yield* callRouter(handler, request, routerContext, controller);
335
+ return toHttpResponse(response, controller);
336
+ }).pipe(Effect.catchDefect((defect) => Effect.logError("React Router adapter failed", defect).pipe(Effect.as(HttpServerResponse.empty({ status: 500 })))));
337
+ export { server_exports as ReactRouterHttp };
@@ -0,0 +1,27 @@
1
+ import { Effect } from "effect";
2
+ import { HttpServerRequest, HttpServerResponse } from "effect/http";
3
+ import * as Http from "node:http";
4
+ declare namespace vite_d_exports {
5
+ export { ViteDevServer, devMiddleware };
6
+ }
7
+ /** The part of a Vite dev server the middleware uses: its Connect middleware stack. */
8
+ interface ViteDevServer {
9
+ readonly middlewares: (request: Http.IncomingMessage, response: Http.ServerResponse, next: (error?: unknown) => void) => void;
10
+ }
11
+ /**
12
+ * Passes each request to Vite first, and the requests Vite passes on to the
13
+ * app. Use it in development only, with Vite in middleware mode:
14
+ *
15
+ * ```ts
16
+ * import { ReactRouterVite } from "react-router-effect/vite";
17
+ *
18
+ * const vite = await createServer({ server: { middlewareMode: true } });
19
+ * HttpServer.serve(Effect.flatten(reactRouterApp).pipe(ReactRouterVite.devMiddleware(vite)));
20
+ * ```
21
+ *
22
+ * A request Vite answers gets an empty `HttpServerResponse`, which Effect's
23
+ * Node server does not write, since Vite has ended the Node response. A Vite
24
+ * error fails the request as a defect.
25
+ */
26
+ declare const devMiddleware: (vite: ViteDevServer) => <E, R>(app: Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>) => Effect.Effect<HttpServerResponse.HttpServerResponse, E, R | HttpServerRequest.HttpServerRequest>;
27
+ export { vite_d_exports as ReactRouterVite };
package/dist/vite.mjs ADDED
@@ -0,0 +1,46 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.mjs";
2
+ import { Effect } from "effect";
3
+ import { HttpServerRequest, HttpServerResponse } from "effect/http";
4
+ import * as NodeHttpServerRequest from "@effect/platform-node/NodeHttpServerRequest";
5
+ // src/vite.ts
6
+ var vite_exports = /* @__PURE__ */ __exportAll({ devMiddleware: () => devMiddleware });
7
+ /**
8
+ * Runs Vite's middleware on the Node request. Succeeds with `true` when Vite
9
+ * passes the request on, and `false` when Vite answered it, or the client left
10
+ * while Vite answered.
11
+ */
12
+ const passThroughVite = (vite, request, response) => Effect.callback((resume) => {
13
+ let settled = false;
14
+ const settle = (result) => {
15
+ if (settled) return;
16
+ settled = true;
17
+ response.off("finish", answered);
18
+ response.off("close", answered);
19
+ resume(result);
20
+ };
21
+ const answered = () => settle(Effect.succeed(false));
22
+ response.once("finish", answered);
23
+ response.once("close", answered);
24
+ try {
25
+ vite.middlewares(request, response, (error) => settle(error === void 0 ? Effect.succeed(true) : Effect.die(error)));
26
+ } catch (error) {
27
+ settle(Effect.die(error));
28
+ }
29
+ });
30
+ /**
31
+ * Passes each request to Vite first, and the requests Vite passes on to the
32
+ * app. Use it in development only, with Vite in middleware mode:
33
+ *
34
+ * ```ts
35
+ * import { ReactRouterVite } from "react-router-effect/vite";
36
+ *
37
+ * const vite = await createServer({ server: { middlewareMode: true } });
38
+ * HttpServer.serve(Effect.flatten(reactRouterApp).pipe(ReactRouterVite.devMiddleware(vite)));
39
+ * ```
40
+ *
41
+ * A request Vite answers gets an empty `HttpServerResponse`, which Effect's
42
+ * Node server does not write, since Vite has ended the Node response. A Vite
43
+ * error fails the request as a defect.
44
+ */
45
+ const devMiddleware = (vite) => (app) => HttpServerRequest.HttpServerRequest.use((request) => passThroughVite(vite, NodeHttpServerRequest.toIncomingMessage(request), NodeHttpServerRequest.toServerResponse(request)).pipe(Effect.flatMap((passed) => passed ? app : Effect.succeed(HttpServerResponse.empty()))));
46
+ export { vite_exports as ReactRouterVite };