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
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { Context, Effect, ManagedRuntime } from "effect";
|
|
2
|
+
import { ActionFunctionArgs, LoaderFunctionArgs, MiddlewareFunction, RouterContext, RouterContextProvider, UNSAFE_DataWithResponseInit, useLoaderData } from "react-router";
|
|
3
|
+
import { HttpServerRespondable } from "effect/http";
|
|
4
|
+
/** The key that holds the metadata in action data. */
|
|
5
|
+
declare const actionMetaKey = "__meta";
|
|
6
|
+
/**
|
|
7
|
+
* Adds optional metadata to a record. Only records get metadata, because a
|
|
8
|
+
* structural type cannot tell a plain object from a class instance, and other
|
|
9
|
+
* values (strings, arrays, `Response`s) have nowhere to put it.
|
|
10
|
+
*/
|
|
11
|
+
type RecordWithMeta<T, Meta> = T extends Record<string, unknown> ? Omit<T, typeof actionMetaKey> & {
|
|
12
|
+
[actionMetaKey]?: Meta;
|
|
13
|
+
} : T;
|
|
14
|
+
/**
|
|
15
|
+
* The type of action data after metadata is attached. A `data(...)` wrapper
|
|
16
|
+
* gets metadata on its body. With no configured metadata (`Meta` is `never`)
|
|
17
|
+
* the data is unchanged.
|
|
18
|
+
*/
|
|
19
|
+
type WithActionMeta<T, Meta> = [Meta] extends [never] ? T : T extends UNSAFE_DataWithResponseInit<infer Body> ? UNSAFE_DataWithResponseInit<RecordWithMeta<Body, Meta>> : RecordWithMeta<T, Meta>;
|
|
20
|
+
declare const ReturnableDataError_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]; }>) => import("effect/Cause").YieldableError & {
|
|
21
|
+
readonly _tag: "ReturnableRouteError";
|
|
22
|
+
} & Readonly<A>;
|
|
23
|
+
/** Recoverable: short-circuits the loader but returns `data(...)` the component reads. */
|
|
24
|
+
declare class ReturnableDataError<D> extends ReturnableDataError_base<{
|
|
25
|
+
data: D;
|
|
26
|
+
init?: number | ResponseInit;
|
|
27
|
+
}> {}
|
|
28
|
+
declare const ThrowableDataError_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]; }>) => import("effect/Cause").YieldableError & {
|
|
29
|
+
readonly _tag: "ThrowableRouteError";
|
|
30
|
+
} & Readonly<A>;
|
|
31
|
+
/** Throwable: rejects with `data(...)` → boundary as an `ErrorResponse`. */
|
|
32
|
+
declare class ThrowableDataError<D> extends ThrowableDataError_base<{
|
|
33
|
+
data: D;
|
|
34
|
+
init?: number | ResponseInit;
|
|
35
|
+
}> {}
|
|
36
|
+
declare const ThrowableRedirectError_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]; }>) => import("effect/Cause").YieldableError & {
|
|
37
|
+
readonly _tag: "ThrowableRedirectError";
|
|
38
|
+
} & Readonly<A>;
|
|
39
|
+
/** Throwable: rejects with a redirect `Response`. */
|
|
40
|
+
declare class ThrowableRedirectError extends ThrowableRedirectError_base<{
|
|
41
|
+
url: string;
|
|
42
|
+
init?: number | ResponseInit;
|
|
43
|
+
}> {}
|
|
44
|
+
/** Every library route error, as a single union. */
|
|
45
|
+
type AnyRouteError = ReturnableDataError<unknown> | ThrowableDataError<unknown> | ThrowableRedirectError;
|
|
46
|
+
/**
|
|
47
|
+
* The library's base helpers for raising route errors (`early` / `throw` /
|
|
48
|
+
* `redirect`). This is internal: it isn't exported from the package. The factory
|
|
49
|
+
* passes it to your config builder and returns it (merged with any `respond`
|
|
50
|
+
* extensions) so your app imports a single `Respond`:
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* export const { makeLoader, Respond } = makeEffectRouteFactory<DomainErrors>()(
|
|
54
|
+
* (Respond) => ({
|
|
55
|
+
* respond: { formError: (reply) => new FormError({ reply }) },
|
|
56
|
+
* errorHandlers: { FormError: (e) => Respond.early({ reply: e.reply }) },
|
|
57
|
+
* }),
|
|
58
|
+
* );
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
declare const Respond: {
|
|
62
|
+
/** Recover: short-circuit and pass `value` to the component as loader/action data. */
|
|
63
|
+
early: <D>(value: D, init?: number | ResponseInit) => ReturnableDataError<D>;
|
|
64
|
+
/** Throw: short-circuit to the error boundary with `data(value, init)`. */
|
|
65
|
+
throw: <D>(value: D, init?: number | ResponseInit) => ThrowableDataError<D>;
|
|
66
|
+
/** Throw: short-circuit with a redirect `Response`. */
|
|
67
|
+
redirect: (url: string, init?: number | ResponseInit) => ThrowableRedirectError;
|
|
68
|
+
};
|
|
69
|
+
/** Narrows an unknown value to one of the library's route errors. */
|
|
70
|
+
declare const isRouteError: (value: unknown) => value is AnyRouteError;
|
|
71
|
+
/** The library's base `Respond` helpers (`early` / `throw` / `redirect`). */
|
|
72
|
+
type BaseRespond = typeof Respond;
|
|
73
|
+
type Tagged = {
|
|
74
|
+
readonly _tag: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Everything a failure branch rejects with. React Router routes a thrown
|
|
78
|
+
* `DataWithResponseInit` to the boundary as an `ErrorResponse`, and a thrown
|
|
79
|
+
* `Response` (including `redirect(...)`) as-is. Pinning every `Effect.fail` branch
|
|
80
|
+
* to this union keeps inference from fixing the error channel to the first one.
|
|
81
|
+
*/
|
|
82
|
+
type FailureResponse = UNSAFE_DataWithResponseInit<unknown> | Response;
|
|
83
|
+
/**
|
|
84
|
+
* What a handler may return, to *remap* a domain error:
|
|
85
|
+
* - a **library route error** - `Respond.early(value)` (recover), `Respond.throw(data)`
|
|
86
|
+
* or `Respond.redirect(url)` (throw) - which the library then processes; or
|
|
87
|
+
* - an **`Effect`** - `Effect.succeed(value)` to recover with `value`, or
|
|
88
|
+
* `Effect.fail(response)` to throw `response`.
|
|
89
|
+
*/
|
|
90
|
+
type ErrorHandler<Err> = (error: Err) => AnyRouteError | Effect.Effect<unknown, FailureResponse>;
|
|
91
|
+
/** The body of every `ReturnableDataError` raised directly in `E` (`Respond.early`). */
|
|
92
|
+
type ReturnableBodyOf<E> = E extends ReturnableDataError<infer Body> ? Body : never;
|
|
93
|
+
/**
|
|
94
|
+
* The recover contribution of a `Respond.early` raised directly in the loader.
|
|
95
|
+
* Guarded so a loader that never recovers directly doesn't leak
|
|
96
|
+
* `DataWithResponseInit<never>` into its return type.
|
|
97
|
+
*/
|
|
98
|
+
type DirectRecover<E> = [ReturnableBodyOf<E>] extends [never] ? never : UNSAFE_DataWithResponseInit<ReturnableBodyOf<E>>;
|
|
99
|
+
/** What a handler may return: a library route error or an `Effect` failing with a response. */
|
|
100
|
+
type HandlerReturn = AnyRouteError | Effect.Effect<unknown, FailureResponse>;
|
|
101
|
+
/** A `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) - `(args, next) => ...`. */
|
|
102
|
+
type AnyMiddlewareFunction = (...args: never[]) => unknown;
|
|
103
|
+
/**
|
|
104
|
+
* The `args` a `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) is called
|
|
105
|
+
* with - inferred from the type argument so `makeMiddleware` can be pinned to a specific
|
|
106
|
+
* generated route's middleware type.
|
|
107
|
+
*/
|
|
108
|
+
type MiddlewareArgsOf<Middleware extends AnyMiddlewareFunction> = Parameters<Middleware>[0];
|
|
109
|
+
/**
|
|
110
|
+
* The `next` a `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) is called
|
|
111
|
+
* with - react-router's own `() => Promise<Result>`, not exported under its own name.
|
|
112
|
+
*/
|
|
113
|
+
type MiddlewareNextOf<Middleware extends AnyMiddlewareFunction> = Parameters<Middleware>[1];
|
|
114
|
+
/**
|
|
115
|
+
* The `Result` a `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) resolves
|
|
116
|
+
* with - i.e. what its `next()` resolves to, and what the middleware itself may return.
|
|
117
|
+
* Read through `Awaited` because react-router declares `next` as an *interface* with a
|
|
118
|
+
* call signature (`MiddlewareNextFunction`), and a route's own may resolve synchronously.
|
|
119
|
+
*/
|
|
120
|
+
type MiddlewareResultOf<Middleware extends AnyMiddlewareFunction> = MiddlewareNextOf<Middleware> extends ((...args: never[]) => infer Result) ? Awaited<Result> : never;
|
|
121
|
+
/**
|
|
122
|
+
* The inferred map of handler RETURN types, keyed by domain-error tag. Making the
|
|
123
|
+
* factory generic over *this* (rather than the handler map as a whole) is what lets
|
|
124
|
+
* `errorHandlers` contextually type each handler's `error` param from its key -
|
|
125
|
+
* no annotation needed - while still capturing precise return types for recovery.
|
|
126
|
+
*/
|
|
127
|
+
type HandlerReturns<DomainError extends Tagged> = Partial<Record<DomainError["_tag"], HandlerReturn>>;
|
|
128
|
+
/**
|
|
129
|
+
* The `errorHandlers` field shape for a given return map: one entry per registered
|
|
130
|
+
* tag, whose `error` is the matching domain error (so it autocompletes and types
|
|
131
|
+
* itself) and whose return is the precise, inferred `Returns[Tag]`.
|
|
132
|
+
*/
|
|
133
|
+
type ErrorHandlers<DomainError extends Tagged, Returns> = { [Tag in keyof Returns]: (error: Extract<DomainError, {
|
|
134
|
+
readonly _tag: Tag;
|
|
135
|
+
}>) => Returns[Tag]; };
|
|
136
|
+
/**
|
|
137
|
+
* What the registered handlers recover into loader/action data - from each
|
|
138
|
+
* handler's return type, but only for handlers whose error can actually occur in
|
|
139
|
+
* `E`:
|
|
140
|
+
* - a remapped `ReturnableDataError<B>` recovers as `DataWithResponseInit<B>`;
|
|
141
|
+
* - an `Effect.succeed(value)` recovers as `value`;
|
|
142
|
+
* - throwables, redirects and `Effect.fail(...)` contribute nothing.
|
|
143
|
+
*/
|
|
144
|
+
type RecoverOf<DomainError extends Tagged, Returns, E> = { [Tag in keyof Returns]: [Extract<E, Extract<DomainError, {
|
|
145
|
+
readonly _tag: Tag;
|
|
146
|
+
}>>] extends [never] ? never : (Returns[Tag] extends ReturnableDataError<infer B> ? UNSAFE_DataWithResponseInit<B> : never) | (Returns[Tag] extends Effect.Effect<infer SuccessValue, any, any> ? SuccessValue : never); }[keyof Returns];
|
|
147
|
+
/**
|
|
148
|
+
* Errors the library deals with on the handler's behalf - so the loader/action/
|
|
149
|
+
* middleware needn't handle them itself:
|
|
150
|
+
* - the app's **declared domain errors** (`DomainError`) - handled by a registered
|
|
151
|
+
* handler, or left to the 500 / auto-respond default;
|
|
152
|
+
* - **library route errors** raised directly via `Respond`;
|
|
153
|
+
* - anything that renders itself via **`HttpServerRespondable`**;
|
|
154
|
+
* - `AlsoHandled` - extra, call-site-specific already-handled errors. Only
|
|
155
|
+
* `makeMiddleware` uses this, to admit the `FailureResponse` its `next()`
|
|
156
|
+
* fails with (a downstream throw, already a finished response - the library
|
|
157
|
+
* re-throws it as-is, so the middleware needn't catch it just to re-raise it).
|
|
158
|
+
*/
|
|
159
|
+
type LibraryHandled<DomainError, AlsoHandled = never> = DomainError | AnyRouteError | HttpServerRespondable.Respondable | AlsoHandled;
|
|
160
|
+
/**
|
|
161
|
+
* What remains in a handler's error channel that the library will NOT handle -
|
|
162
|
+
* i.e. service-specific errors the route consumes that aren't declared domain
|
|
163
|
+
* errors. The loader/action/middleware must handle these itself (catch or map them).
|
|
164
|
+
*/
|
|
165
|
+
type Unhandled<E, DomainError, AlsoHandled = never> = Exclude<E, LibraryHandled<DomainError, AlsoHandled>>;
|
|
166
|
+
/**
|
|
167
|
+
* A readable compile-time diagnostic. It's intersected onto the *effect return type*
|
|
168
|
+
* of `fn` - so the effect's `A`/`E`/`R` still infer directly (naked), while a mismatch
|
|
169
|
+
* reports this message, and the offending types, *at the `makeLoader`/`makeAction`
|
|
170
|
+
* call* instead of the opaque "Expected 2 arguments". Resolves to `unknown` (a no-op
|
|
171
|
+
* intersection) when the effect is fully handled and all its requirements are provided:
|
|
172
|
+
*
|
|
173
|
+
* - **Unhandled error** - the effect can fail with something that isn't a declared
|
|
174
|
+
* domain error, a library route error, or respondable; `unhandledErrors` lists them.
|
|
175
|
+
* - **Missing requirement** - the effect requires a service that neither the
|
|
176
|
+
* `runtime` nor the `requestContext` provides; `missingRequirements` lists them.
|
|
177
|
+
*
|
|
178
|
+
* The message is in an *inline* object literal (no named alias) so tsc/tsgo print
|
|
179
|
+
* its text verbatim rather than collapsing it to an alias name.
|
|
180
|
+
*/
|
|
181
|
+
/**
|
|
182
|
+
* Lists the unhandled errors for the diagnostic: a tagged error becomes its `_tag`
|
|
183
|
+
* (the string you'd pass to `Effect.catchTag`), anything untagged stays as its type.
|
|
184
|
+
*/
|
|
185
|
+
type NameError<U> = U extends {
|
|
186
|
+
readonly _tag: infer Tag extends string;
|
|
187
|
+
} ? Tag : U;
|
|
188
|
+
/**
|
|
189
|
+
* True exactly when `T` is (or, for a union, includes) `any` - not `unknown`, not
|
|
190
|
+
* `never`, not any other type. An unrelated mistake elsewhere in the effect (a typo,
|
|
191
|
+
* a call with the wrong number of arguments, a genuine syntax error) commonly makes
|
|
192
|
+
* TypeScript infer `any` for `E`/`R` instead of reporting a clean type; running
|
|
193
|
+
* `Diagnose` on that `any` produces a confusing "unhandled errors: any" message that
|
|
194
|
+
* buries the actual mistake. Guarding on this and bailing to `unknown` lets tsc's own,
|
|
195
|
+
* on-site error appear instead. (`1 & T` is `any` when `T` is `any` - including when
|
|
196
|
+
* `any` only appears as a union member, since intersection distributes over unions -
|
|
197
|
+
* and `0` is assignable to `any` but not to any concrete type built from `1 & T`.)
|
|
198
|
+
*/
|
|
199
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
200
|
+
type Diagnose<E, R, DomainError, Provided, AlsoHandled = never> = IsAny<E> extends true ? unknown : IsAny<R> extends true ? unknown : [Unhandled<E, DomainError, AlsoHandled>] extends [never] ? [Exclude<R, Provided>] extends [never] ? unknown : {
|
|
201
|
+
"react-router-effect": "This loader/action/middleware requires a service the factory does not provide. Add it to your runtime or requestContext, or handle it in the effect.";
|
|
202
|
+
missingRequirements: Exclude<R, Provided>;
|
|
203
|
+
} : {
|
|
204
|
+
"react-router-effect": "This loader/action/middleware can fail with an error the library does not handle. Catch it in the effect (e.g. Effect.catchTag), make it Respondable, or add it to your DomainError union.";
|
|
205
|
+
unhandledErrors: NameError<Unhandled<E, DomainError, AlsoHandled>>;
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* A React Router context key holding a per-request effect `Context.Context`. Set
|
|
209
|
+
* it in middleware and pass it to the factory's `requestContext` - the runner
|
|
210
|
+
* reads it on every request and provides its services to the loader/action.
|
|
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
|
+
*
|
|
218
|
+
* It's just a `RouterContext`; create one with React Router's `createContext`:
|
|
219
|
+
*
|
|
220
|
+
* ```ts
|
|
221
|
+
* import { createContext } from "react-router";
|
|
222
|
+
* import { Context } from "effect";
|
|
223
|
+
* import type { RequestContextKey } from "react-router-effect";
|
|
224
|
+
*
|
|
225
|
+
* class RequestContext extends Context.Service<RequestContext, {
|
|
226
|
+
* readonly userId: string;
|
|
227
|
+
* }>()("app/RequestContext") {}
|
|
228
|
+
*
|
|
229
|
+
* export const requestContext: RequestContextKey<RequestContext> = createContext();
|
|
230
|
+
*
|
|
231
|
+
* // middleware, in an app without the server adapter:
|
|
232
|
+
* export const middleware: Route.MiddlewareFunction[] = [
|
|
233
|
+
* ({ context, request }, next) => {
|
|
234
|
+
* context.set(requestContext, Context.make(RequestContext, { userId: readUser(request) }));
|
|
235
|
+
* return next();
|
|
236
|
+
* },
|
|
237
|
+
* ];
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqServices>>;
|
|
241
|
+
/** What `actionMeta` reads: the request and the router context of the action. */
|
|
242
|
+
interface ActionMetaArgs {
|
|
243
|
+
readonly request: Request;
|
|
244
|
+
readonly context: Readonly<RouterContextProvider>;
|
|
245
|
+
}
|
|
246
|
+
/** The `next` an effect middleware gets: runs the rest of the chain as an effect. */
|
|
247
|
+
type EffectNext<Result> = () => Effect.Effect<Result, FailureResponse>;
|
|
248
|
+
/**
|
|
249
|
+
* The arguments of an effect middleware: React Router's own middleware args,
|
|
250
|
+
* plus `next` as an effect. The second parameter stays React Router's
|
|
251
|
+
* promise-returning `next`, so a plain middleware works unchanged.
|
|
252
|
+
*/
|
|
253
|
+
type EffectMiddlewareArgs<Middleware extends AnyMiddlewareFunction = MiddlewareFunction> = MiddlewareArgsOf<Middleware> & {
|
|
254
|
+
readonly next: EffectNext<MiddlewareResultOf<Middleware>>;
|
|
255
|
+
};
|
|
256
|
+
/** The router context and signal every route function receives. */
|
|
257
|
+
interface RouteArgs {
|
|
258
|
+
readonly request: Request;
|
|
259
|
+
readonly context: Readonly<RouterContextProvider>;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* What a route function resolves with: an Effect's success, or the awaited
|
|
263
|
+
* value. A failure that a handler recovers resolves with its data too.
|
|
264
|
+
*/
|
|
265
|
+
type RouteResult<Out> = Out extends Effect.Effect<infer A, infer _E, infer _R> ? A : Awaited<Out>;
|
|
266
|
+
/**
|
|
267
|
+
* Turns route functions into the promise-returning functions React Router
|
|
268
|
+
* calls. An Effect result runs through the factory's error model with the
|
|
269
|
+
* factory's runtime and the request's effect context; any other result passes
|
|
270
|
+
* through. The server adapter applies these to every route module, and tests
|
|
271
|
+
* can call them directly.
|
|
272
|
+
*
|
|
273
|
+
* Each is generic over the route's own args, so a test can pass a route's
|
|
274
|
+
* `Route.LoaderArgs` without a cast. React Router always supplies `request`
|
|
275
|
+
* and `context`.
|
|
276
|
+
*/
|
|
277
|
+
interface RouteHandlers {
|
|
278
|
+
readonly loader: <Args extends RouteArgs, Out>(fn: (args: Args) => Out) => (args: Args) => Promise<RouteResult<Out>>;
|
|
279
|
+
readonly action: <Args extends RouteArgs, Out>(fn: (args: Args) => Out) => (args: Args) => Promise<RouteResult<Out>>;
|
|
280
|
+
/**
|
|
281
|
+
* The wrapped middleware takes React Router's args, without `next`: the
|
|
282
|
+
* runner adds `next` as an effect.
|
|
283
|
+
*/
|
|
284
|
+
readonly middleware: <Args extends RouteArgs, Next extends () => Promise<unknown>, Out>(fn: (args: Args, next: Next) => Out) => (args: Omit<Args, "next">, next: Next) => Promise<RouteResult<Out> | undefined>;
|
|
285
|
+
/** The router context key that holds the request's effect context. */
|
|
286
|
+
readonly requestContext: RouterContext<Context.Context<never>>;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* The types a factory exposes for the server adapter's route check and for
|
|
290
|
+
* `Register`. Never present at runtime.
|
|
291
|
+
*/
|
|
292
|
+
interface FactoryTypes<DomainError, Returns, Services, Meta> {
|
|
293
|
+
readonly DomainError: DomainError;
|
|
294
|
+
readonly Returns: Returns;
|
|
295
|
+
readonly Services: Services;
|
|
296
|
+
readonly Meta: Meta;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Build `makeLoader` / `makeAction` / `makeMiddleware` for an application, wired to
|
|
300
|
+
* its domain errors.
|
|
301
|
+
*
|
|
302
|
+
* Declare the app's **domain errors** as the type argument, then register an
|
|
303
|
+
* *optional* handler per domain error in `errorHandlers`. The handler keys
|
|
304
|
+
* autocomplete to your domain-error tags and each `error` parameter is typed from
|
|
305
|
+
* its key - no annotation needed. It's curried so you can pin the domain errors
|
|
306
|
+
* while the handler return types are still inferred:
|
|
307
|
+
*
|
|
308
|
+
* ```ts
|
|
309
|
+
* type DomainErrors = MyDomainError | DbError | NotAuthorizedError;
|
|
310
|
+
*
|
|
311
|
+
* export const { makeLoader, makeAction, Respond } =
|
|
312
|
+
* makeEffectRouteFactory<DomainErrors>()((Respond) => ({
|
|
313
|
+
* errorHandlers: {
|
|
314
|
+
* // `error` is typed as MyDomainError automatically. throw → error boundary
|
|
315
|
+
* MyDomainError: (error) => Respond.throw({ message: error.message }, 400),
|
|
316
|
+
* // DbError has no handler → falls through to the 500 default.
|
|
317
|
+
* },
|
|
318
|
+
* }));
|
|
319
|
+
* ```
|
|
320
|
+
*
|
|
321
|
+
* The inner config is a **builder** - it receives the library's `Respond` helpers so
|
|
322
|
+
* your handlers can recover/throw with them, and returns the config. `Respond` is
|
|
323
|
+
* also returned from the factory (extended with any `respond` helpers you add), so
|
|
324
|
+
* your app imports a *single* `Respond` - no auto-import ambiguity with a library one.
|
|
325
|
+
*
|
|
326
|
+
* A handler *remaps* the error by returning either a library route error
|
|
327
|
+
* (`Respond.early` to recover; `Respond.throw` / `Respond.redirect` to throw) or an
|
|
328
|
+
* `Effect` (`Effect.succeed(value)` to recover `value`; `Effect.fail(response)` to
|
|
329
|
+
* throw). Recovered values become loader/action data, typed precisely.
|
|
330
|
+
*
|
|
331
|
+
* **Declared domain errors** may be left unhandled: if one implements
|
|
332
|
+
* `HttpServerRespondable` it's rendered automatically, otherwise it falls through
|
|
333
|
+
* to a 500. **Any other error** - a service-specific error the route consumes that
|
|
334
|
+
* isn't a declared domain error - *must* be handled in the loader/action, or
|
|
335
|
+
* `makeLoader`/`makeAction` fails to type-check.
|
|
336
|
+
*
|
|
337
|
+
* Pass a `runtime` (a `ManagedRuntime`, e.g. from `ManagedRuntime.make(AppLayer)`)
|
|
338
|
+
* to provide your app's services once. Loader/action effects may then require those
|
|
339
|
+
* services directly - no per-call `Effect.provide`:
|
|
340
|
+
*
|
|
341
|
+
* ```ts
|
|
342
|
+
* const { makeLoader, makeAction } = makeEffectRouteFactory<DomainErrors>()(() => ({
|
|
343
|
+
* runtime: getAppRuntime(), // provides Database, MyService, ...
|
|
344
|
+
* errorHandlers: { ... },
|
|
345
|
+
* }));
|
|
346
|
+
*
|
|
347
|
+
* // `MyService` is satisfied by the runtime, not provided here:
|
|
348
|
+
* const loader = makeLoader((args: Route.LoaderArgs) =>
|
|
349
|
+
* Effect.gen(function* () {
|
|
350
|
+
* const svc = yield* MyService;
|
|
351
|
+
* return { data: yield* svc.load(args) };
|
|
352
|
+
* }),
|
|
353
|
+
* );
|
|
354
|
+
* ```
|
|
355
|
+
*
|
|
356
|
+
* `makeMiddleware` wraps a `Route.MiddlewareFunction` the same way: write it as an
|
|
357
|
+
* effect, and register the same domain errors/handlers. Pin it to the route's
|
|
358
|
+
* generated `Route.MiddlewareFunction` (a type argument, since middleware has no
|
|
359
|
+
* separate args-only type) to type `args` and `next`'s result from that route; it
|
|
360
|
+
* defaults to React Router's own `MiddlewareFunction`. The `next` handed to the
|
|
361
|
+
* effect is `() => Effect.Effect<Result, FailureResponse>` - call it with `yield*`
|
|
362
|
+
* to run the rest of the chain, and a downstream throw (e.g. a nested loader's
|
|
363
|
+
* redirect) becomes a typed failure instead of a rejected promise:
|
|
364
|
+
*
|
|
365
|
+
* ```ts
|
|
366
|
+
* const middleware = makeMiddleware<Route.MiddlewareFunction>((args, next) =>
|
|
367
|
+
* Effect.gen(function* () {
|
|
368
|
+
* const user = yield* getUser(args); // may fail with a declared domain error
|
|
369
|
+
* if (!user) yield* Respond.redirect("/login");
|
|
370
|
+
* return yield* next();
|
|
371
|
+
* }),
|
|
372
|
+
* );
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
declare function makeEffectRouteFactory<DomainError extends Tagged = never>(): <const Returns extends HandlerReturns<DomainError> = {}, RServices = never, ReqServices = never, const ExtraRespond extends Record<string, (...args: never[]) => unknown> = {}, Meta = never>(builder: (respond: BaseRespond) => {
|
|
376
|
+
/**
|
|
377
|
+
* An optional handler per declared domain error, keyed by its tag. The keys
|
|
378
|
+
* autocomplete to your domain-error tags and each handler's `error` parameter
|
|
379
|
+
* is typed automatically - no annotation needed. A handler *remaps* the error
|
|
380
|
+
* by returning a library route error (the builder's `Respond.early` / `throw`
|
|
381
|
+
* / `redirect`) or an `Effect` (`Effect.succeed`/`fail`). Omit to register none.
|
|
382
|
+
*/
|
|
383
|
+
errorHandlers?: ErrorHandlers<DomainError, Returns>;
|
|
384
|
+
/**
|
|
385
|
+
* The app runtime that provides services to loader/action effects. When
|
|
386
|
+
* set, effects may require its services (`RServices`, inferred from here)
|
|
387
|
+
* without providing layers, and runs go through `runtime.runPromise`. When
|
|
388
|
+
* omitted, `RServices` is `never` and effects must require nothing.
|
|
389
|
+
*/
|
|
390
|
+
runtime?: ManagedRuntime.ManagedRuntime<RServices, any>;
|
|
391
|
+
/**
|
|
392
|
+
* A React Router context key (a {@link RequestContextKey}) holding a
|
|
393
|
+
* per-request effect `Context.Context`. Middleware sets it for each request;
|
|
394
|
+
* the runner reads `args.context.get(requestContext)` and provides those
|
|
395
|
+
* services to the effect. Loader/action effects may then require
|
|
396
|
+
* `ReqServices` (inferred from here) in addition to the runtime's services.
|
|
397
|
+
*/
|
|
398
|
+
requestContext?: RequestContextKey<ReqServices>;
|
|
399
|
+
/**
|
|
400
|
+
* App-specific `Respond` helpers - merged onto the base `Respond` that the
|
|
401
|
+
* factory returns, so your app imports one `Respond`. Typically opinionated
|
|
402
|
+
* error constructors; annotate their parameters, e.g.
|
|
403
|
+
* `{ formError: (reply: string) => new FormError({ reply }) }`. The base
|
|
404
|
+
* helpers (`early` / `throw` / `redirect`) always win the merge.
|
|
405
|
+
*/
|
|
406
|
+
respond?: ExtraRespond;
|
|
407
|
+
/**
|
|
408
|
+
* Request-scoped metadata to attach to every action's data under `__meta`,
|
|
409
|
+
* read after the action resolves. The server adapter attaches it to every
|
|
410
|
+
* action it runs, including plain functions and `makeAction` wrappers. A
|
|
411
|
+
* thrown result gets none. Only plain records and `data(...)` bodies include
|
|
412
|
+
* it; other values are returned unchanged.
|
|
413
|
+
*/
|
|
414
|
+
actionMeta?: (args: ActionMetaArgs) => Meta;
|
|
415
|
+
}) => {
|
|
416
|
+
makeLoader: <Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E, R>(fn: (args: Args) => Effect.Effect<A, E, R> & Diagnose<E, R, DomainError, RServices | ReqServices>) => (args: Args) => Promise<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>>;
|
|
417
|
+
makeAction: <Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E, R>(fn: (args: Args) => Effect.Effect<A, E, R> & Diagnose<E, R, DomainError, RServices | ReqServices>) => (args: Args) => Promise<WithActionMeta<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>, Meta>>;
|
|
418
|
+
makeMiddleware: <Middleware extends AnyMiddlewareFunction = MiddlewareFunction, E = LibraryHandled<DomainError, FailureResponse>, R = RServices | ReqServices>(fn: (args: MiddlewareArgsOf<Middleware>, next: () => Effect.Effect<MiddlewareResultOf<Middleware>, FailureResponse>) => Effect.Effect<MiddlewareResultOf<Middleware> | void, E, R> & Diagnose<E, R, DomainError, RServices | ReqServices, FailureResponse>) => (args: MiddlewareArgsOf<Middleware>, next: MiddlewareNextOf<Middleware>) => Promise<MiddlewareResultOf<Middleware> | undefined>;
|
|
419
|
+
Respond: {
|
|
420
|
+
early: <D>(value: D, init?: number | ResponseInit) => ReturnableDataError<D>;
|
|
421
|
+
throw: <D>(value: D, init?: number | ResponseInit) => ThrowableDataError<D>;
|
|
422
|
+
redirect: (url: string, init?: number | ResponseInit) => ThrowableRedirectError;
|
|
423
|
+
} & ExtraRespond;
|
|
424
|
+
/** The route runner the server adapter applies to every route module. */
|
|
425
|
+
handlers: RouteHandlers;
|
|
426
|
+
/** Type-only: what the adapter's route check and `Register` read. */
|
|
427
|
+
"~types": FactoryTypes<DomainError, Returns, RServices | ReqServices, Meta>;
|
|
428
|
+
};
|
|
429
|
+
/**
|
|
430
|
+
* Registers what the app's route factory turns failures into, so that a
|
|
431
|
+
* loader or action written as a plain `Effect.fn` types its data the way a
|
|
432
|
+
* `makeLoader` call would.
|
|
433
|
+
*
|
|
434
|
+
* A `makeLoader` call sees the factory's handlers as type arguments. A plain
|
|
435
|
+
* Effect loader does not, so without this an error that a handler recovers
|
|
436
|
+
* into data, such as a form error that becomes `{ reply }`, would be missing
|
|
437
|
+
* from `useLoaderData`, `useFetcher` and `Route.ComponentProps`.
|
|
438
|
+
*
|
|
439
|
+
* Declare the types directly rather than reading them from the factory's
|
|
440
|
+
* value: the factory's type depends on every Effect in the app, and so on
|
|
441
|
+
* this registration. The server adapter's `RouteEffectCheck` fails when the
|
|
442
|
+
* registration and the factory disagree.
|
|
443
|
+
*
|
|
444
|
+
* ```ts
|
|
445
|
+
* declare module "react-router-effect" {
|
|
446
|
+
* interface Register {
|
|
447
|
+
* // Each error tag a handler recovers, and the data it recovers into.
|
|
448
|
+
* recover: { FormValidationError: { reply: SubmissionResponse } };
|
|
449
|
+
* // What `actionMeta` returns.
|
|
450
|
+
* actionMeta: { realtimeEventIds: ReadonlyArray<string> };
|
|
451
|
+
* }
|
|
452
|
+
* }
|
|
453
|
+
* ```
|
|
454
|
+
*/
|
|
455
|
+
interface Register$1 {}
|
|
456
|
+
/** The registered recover map: error tag to recovered data. */
|
|
457
|
+
type RegisteredRecover = Register$1 extends {
|
|
458
|
+
readonly recover: infer Recover;
|
|
459
|
+
} ? Recover : {};
|
|
460
|
+
/** The registered action metadata, or `never` when there is none. */
|
|
461
|
+
type RegisteredMeta = Register$1 extends {
|
|
462
|
+
readonly actionMeta: infer Meta;
|
|
463
|
+
} ? Meta : never;
|
|
464
|
+
/** What a failure of type `E` recovers into: `Respond.early` bodies and registered handlers. */
|
|
465
|
+
type Recovered<E> = (E extends ReturnableDataError<infer Body> ? Body : never) | (E extends {
|
|
466
|
+
readonly _tag: infer Tag extends keyof RegisteredRecover;
|
|
467
|
+
} ? RegisteredRecover[Tag] : never);
|
|
468
|
+
/**
|
|
469
|
+
* React Router's own serialization of loader data. React Router does not
|
|
470
|
+
* export it, but a zero-argument function is a server loader to its public
|
|
471
|
+
* `useLoaderData` type, so this gives the same type.
|
|
472
|
+
*/
|
|
473
|
+
type Serialize<T> = ReturnType<typeof useLoaderData<() => Promise<T>>>;
|
|
474
|
+
/**
|
|
475
|
+
* The data an Effect that succeeds with `A` and fails with `E` gives the
|
|
476
|
+
* component: its success and what its failures recover into, serialized as
|
|
477
|
+
* React Router serializes them, with the registered action metadata.
|
|
478
|
+
*/
|
|
479
|
+
type EffectData<A, E> = WithActionMeta<Serialize<A | Recovered<E>>, RegisteredMeta>;
|
|
480
|
+
/**
|
|
481
|
+
* The route data of a loader or action, for React Router's hooks:
|
|
482
|
+
* `useFetcher<RouteData<typeof action>>()`. A function that returns an Effect
|
|
483
|
+
* gives its `EffectData`. Any other function passes through, so the hook
|
|
484
|
+
* reads it as it always has.
|
|
485
|
+
*/
|
|
486
|
+
type RouteData<F> = F extends ((...args: never[]) => Effect.Effect<infer A, infer E, infer _R>) ? EffectData<A, E> : F;
|
|
487
|
+
/**
|
|
488
|
+
* Loader data as React Router reads it before serializing: a `Response` adds
|
|
489
|
+
* nothing, and a `data(...)` wrapper gives its body.
|
|
490
|
+
*/
|
|
491
|
+
type Unwrapped<A> = A extends Response ? never : A extends UNSAFE_DataWithResponseInit<infer Body> ? Body : A;
|
|
492
|
+
/**
|
|
493
|
+
* The data React Router reads from an Effect through `unstable_SerializesTo`.
|
|
494
|
+
*
|
|
495
|
+
* TypeScript compares this brand whenever it relates two Effects
|
|
496
|
+
* structurally, so it must keep the subtype order of `A` and `E`: when
|
|
497
|
+
* `Effect<A1, E1>` is assignable to `Effect<A2, E2>`, `BrandData<A1, E1>` must
|
|
498
|
+
* be assignable to `BrandData<A2, E2>`. React Router's `Serialize` does not
|
|
499
|
+
* (it turns `unknown` into `undefined`), so the brand holds the data before
|
|
500
|
+
* serialization; `RouteData` applies the actual serialization.
|
|
501
|
+
*
|
|
502
|
+
* - A `void` or `any` success gives `unknown`, which accepts every other brand.
|
|
503
|
+
* - A `never` success gives only what the failures recover into.
|
|
504
|
+
*
|
|
505
|
+
* `Effect` declares its parameters `out`, and TypeScript verifies that
|
|
506
|
+
* annotation against this member, and against it again in every Effect type
|
|
507
|
+
* that declares variance (`Exit`, `Config`, ...). It relates two conditional
|
|
508
|
+
* types branch by branch when their check types are related and their
|
|
509
|
+
* `extends` types are identical. So every conditional here, and in the types
|
|
510
|
+
* it uses, checks `A` or `E` (or a tuple of one) against a type that does not
|
|
511
|
+
* depend on them. A conditional with `A` or `E` on its `extends` side, such as
|
|
512
|
+
* `0 extends 1 & A` to detect `any`, fails the verification: a consumer that
|
|
513
|
+
* checks library types (`skipLibCheck: false`) then gets TS2636 in Effect's own
|
|
514
|
+
* declarations. `test/consumer` checks the built package for that.
|
|
515
|
+
*/
|
|
516
|
+
type BrandData<A, E> = [A] extends [never] ? WithActionMeta<Recovered<E>, RegisteredMeta> : [A] extends [void] ? unknown : WithActionMeta<Unwrapped<A> | Recovered<E>, RegisteredMeta>;
|
|
517
|
+
declare module "effect/Effect" {
|
|
518
|
+
interface Effect<out A, out E, out R> {
|
|
519
|
+
readonly unstable__ReactRouter_SerializesTo: [BrandData<A, E>];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
declare module "effect/Cause" {
|
|
523
|
+
interface YieldableError {
|
|
524
|
+
readonly unstable__ReactRouter_SerializesTo: [never];
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Route args for a loader or action written as a plain `Effect.fn`, such as
|
|
529
|
+
* `EffectRouteArgs<Route.ActionArgs>`. It keeps hooks that take `typeof loader`
|
|
530
|
+
* or `typeof action` typed, such as `useFetcher<typeof action>()`.
|
|
531
|
+
*
|
|
532
|
+
* Those hooks read a function's data through React Router's `SerializeFrom`.
|
|
533
|
+
* It treats a function whose argument extends `ClientDataFunctionArgs` as a
|
|
534
|
+
* client function, and gives its raw return value without serialization.
|
|
535
|
+
* Typegen's `Route.LoaderArgs` and `Route.ActionArgs` have the same members as
|
|
536
|
+
* `ClientDataFunctionArgs`, so a server function typed with them matches too.
|
|
537
|
+
* For an Effect, the raw return value is the Effect itself: only serialization
|
|
538
|
+
* reads the `unstable_SerializesTo` brand.
|
|
539
|
+
*
|
|
540
|
+
* `pattern` is optional here, so the argument no longer matches and the hooks
|
|
541
|
+
* read the brand. `Route.ComponentProps` and `RouteData` don't go through
|
|
542
|
+
* `SerializeFrom`, so they are correct without it.
|
|
543
|
+
*/
|
|
544
|
+
type EffectRouteArgs<Args extends {
|
|
545
|
+
readonly pattern: string;
|
|
546
|
+
}> = Omit<Args, "pattern"> & {
|
|
547
|
+
readonly pattern?: string;
|
|
548
|
+
};
|
|
549
|
+
export { ReturnableDataError as C, WithActionMeta as D, isRouteError as E, actionMetaKey as O, AnyRouteError as S, ThrowableRedirectError as T, NameError as _, RegisteredMeta as a, RouteResult as b, Serialize as c, EffectNext as d, ErrorHandler as f, LibraryHandled as g, IsAny as h, Register$1 as i, ActionMetaArgs as l, FailureResponse as m, EffectData as n, RegisteredRecover as o, FactoryTypes as p, EffectRouteArgs as r, RouteData as s, BrandData as t, EffectMiddlewareArgs as u, RequestContextKey as v, ThrowableDataError as w, makeEffectRouteFactory as x, RouteHandlers as y };
|