react-router-effect 1.0.2 → 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.
- package/README.md +548 -173
- package/dist/index.d.mts +2 -342
- package/dist/index.mjs +122 -34
- package/dist/rolldown-runtime-D8jQpDG7.mjs +12 -0
- package/dist/route-data-MgSnnR33.d.mts +543 -0
- package/dist/server.d.mts +218 -0
- package/dist/server.mjs +337 -0
- package/dist/vite.d.mts +27 -0
- package/dist/vite.mjs +46 -0
- package/package.json +22 -10
- package/src/action-meta.ts +58 -0
- package/src/check.ts +205 -0
- package/src/errors.ts +3 -3
- package/src/factory.ts +291 -84
- package/src/index.ts +22 -1
- package/src/route-data.ts +151 -0
- package/src/server-entry.ts +10 -0
- package/src/server.ts +647 -0
- package/src/vite-entry.ts +2 -0
- package/src/vite.ts +92 -0
package/dist/index.d.mts
CHANGED
|
@@ -1,342 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { ActionFunctionArgs, LoaderFunctionArgs, MiddlewareFunction, RouterContext, UNSAFE_DataWithResponseInit } from "react-router";
|
|
4
|
-
//#region src/errors.d.ts
|
|
5
|
-
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 & {
|
|
6
|
-
readonly _tag: "ReturnableRouteError";
|
|
7
|
-
} & Readonly<A>;
|
|
8
|
-
/** Recoverable: short-circuits the loader but returns `data(...)` the component reads. */
|
|
9
|
-
declare class ReturnableDataError<D> extends ReturnableDataError_base<{
|
|
10
|
-
data: D;
|
|
11
|
-
init?: number | ResponseInit;
|
|
12
|
-
}> {}
|
|
13
|
-
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 & {
|
|
14
|
-
readonly _tag: "ThrowableRouteError";
|
|
15
|
-
} & Readonly<A>;
|
|
16
|
-
/** Throwable: rejects with `data(...)` → boundary as an `ErrorResponse`. */
|
|
17
|
-
declare class ThrowableDataError<D> extends ThrowableDataError_base<{
|
|
18
|
-
data: D;
|
|
19
|
-
init?: number | ResponseInit;
|
|
20
|
-
}> {}
|
|
21
|
-
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 & {
|
|
22
|
-
readonly _tag: "ThrowableRedirectError";
|
|
23
|
-
} & Readonly<A>;
|
|
24
|
-
/** Throwable: rejects with a redirect `Response`. */
|
|
25
|
-
declare class ThrowableRedirectError extends ThrowableRedirectError_base<{
|
|
26
|
-
url: string;
|
|
27
|
-
init?: number | ResponseInit;
|
|
28
|
-
}> {}
|
|
29
|
-
/** Every library route error, as a single union. */
|
|
30
|
-
type AnyRouteError = ReturnableDataError<unknown> | ThrowableDataError<unknown> | ThrowableRedirectError;
|
|
31
|
-
/**
|
|
32
|
-
* The library's base helpers for raising route errors (`early` / `throw` /
|
|
33
|
-
* `redirect`). This is internal: it isn't exported from the package. The factory
|
|
34
|
-
* hands it to your config builder and returns it (merged with any `respond`
|
|
35
|
-
* extensions) so your app imports a single `Respond`:
|
|
36
|
-
*
|
|
37
|
-
* ```ts
|
|
38
|
-
* export const { makeLoader, Respond } = makeEffectRouteFactory<DomainErrors>()(
|
|
39
|
-
* (Respond) => ({
|
|
40
|
-
* respond: { formError: (reply) => new FormError({ reply }) },
|
|
41
|
-
* errorHandlers: { FormError: (e) => Respond.early({ reply: e.reply }) },
|
|
42
|
-
* }),
|
|
43
|
-
* );
|
|
44
|
-
* ```
|
|
45
|
-
*/
|
|
46
|
-
declare const Respond: {
|
|
47
|
-
/** Recover: short-circuit and hand `value` to the component as loader/action data. */
|
|
48
|
-
early: <D>(value: D, init?: number | ResponseInit) => ReturnableDataError<D>;
|
|
49
|
-
/** Throw: short-circuit to the error boundary with `data(value, init)`. */
|
|
50
|
-
throw: <D>(value: D, init?: number | ResponseInit) => ThrowableDataError<D>;
|
|
51
|
-
/** Throw: short-circuit with a redirect `Response`. */
|
|
52
|
-
redirect: (url: string, init?: number | ResponseInit) => ThrowableRedirectError;
|
|
53
|
-
};
|
|
54
|
-
/** Narrows an unknown value to one of the library's route errors. */
|
|
55
|
-
declare const isRouteError: (value: unknown) => value is AnyRouteError;
|
|
56
|
-
//#endregion
|
|
57
|
-
//#region src/factory.d.ts
|
|
58
|
-
/** The library's base `Respond` helpers (`early` / `throw` / `redirect`). */
|
|
59
|
-
type BaseRespond = typeof Respond;
|
|
60
|
-
type Tagged = {
|
|
61
|
-
readonly _tag: string;
|
|
62
|
-
};
|
|
63
|
-
/**
|
|
64
|
-
* Everything a failure branch rejects with. React Router routes a thrown
|
|
65
|
-
* `DataWithResponseInit` to the boundary as an `ErrorResponse`, and a thrown
|
|
66
|
-
* `Response` (including `redirect(...)`) as-is. Pinning every `Effect.fail` branch
|
|
67
|
-
* to this union keeps inference from fixing the error channel to the first one.
|
|
68
|
-
*/
|
|
69
|
-
type FailureResponse = UNSAFE_DataWithResponseInit<unknown> | Response;
|
|
70
|
-
/**
|
|
71
|
-
* What a handler may return, to *remap* a domain error:
|
|
72
|
-
* - a **library route error** — `Respond.early(value)` (recover), `Respond.throw(data)`
|
|
73
|
-
* or `Respond.redirect(url)` (throw) — which the library then processes; or
|
|
74
|
-
* - an **`Effect`** — `Effect.succeed(value)` to recover with `value`, or
|
|
75
|
-
* `Effect.fail(response)` to throw `response`.
|
|
76
|
-
*/
|
|
77
|
-
type ErrorHandler<Err> = (error: Err) => AnyRouteError | Effect.Effect<unknown, FailureResponse>;
|
|
78
|
-
/** The body of every `ReturnableDataError` raised directly in `E` (`Respond.early`). */
|
|
79
|
-
type ReturnableBodyOf<E> = E extends ReturnableDataError<infer Body> ? Body : never;
|
|
80
|
-
/**
|
|
81
|
-
* The recover contribution of a `Respond.early` raised directly in the loader.
|
|
82
|
-
* Guarded so a loader that never recovers directly doesn't leak
|
|
83
|
-
* `DataWithResponseInit<never>` into its return type.
|
|
84
|
-
*/
|
|
85
|
-
type DirectRecover<E> = [ReturnableBodyOf<E>] extends [never] ? never : UNSAFE_DataWithResponseInit<ReturnableBodyOf<E>>;
|
|
86
|
-
/** What a handler may return: a library route error or an `Effect` failing with a response. */
|
|
87
|
-
type HandlerReturn = AnyRouteError | Effect.Effect<unknown, FailureResponse>;
|
|
88
|
-
/** A `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) — `(args, next) => ...`. */
|
|
89
|
-
type AnyMiddlewareFunction = (...args: never[]) => unknown;
|
|
90
|
-
/**
|
|
91
|
-
* The `args` a `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) is called
|
|
92
|
-
* with — inferred from the type argument so `makeMiddleware` can be pinned to a specific
|
|
93
|
-
* generated route's middleware type.
|
|
94
|
-
*/
|
|
95
|
-
type MiddlewareArgsOf<Middleware extends AnyMiddlewareFunction> = Parameters<Middleware>[0];
|
|
96
|
-
/**
|
|
97
|
-
* The `next` a `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) is called
|
|
98
|
-
* with — react-router's own `() => Promise<Result>`, not exported under its own name.
|
|
99
|
-
*/
|
|
100
|
-
type MiddlewareNextOf<Middleware extends AnyMiddlewareFunction> = Parameters<Middleware>[1];
|
|
101
|
-
/**
|
|
102
|
-
* The `Result` a `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) resolves
|
|
103
|
-
* with — i.e. what its `next()` resolves to, and what the middleware itself may return.
|
|
104
|
-
* Read through `Awaited` because react-router declares `next` as an *interface* with a
|
|
105
|
-
* call signature (`MiddlewareNextFunction`), and a route's own may resolve synchronously.
|
|
106
|
-
*/
|
|
107
|
-
type MiddlewareResultOf<Middleware extends AnyMiddlewareFunction> = MiddlewareNextOf<Middleware> extends ((...args: never[]) => infer Result) ? Awaited<Result> : never;
|
|
108
|
-
/**
|
|
109
|
-
* The inferred map of handler RETURN types, keyed by domain-error tag. Making the
|
|
110
|
-
* factory generic over *this* (rather than the whole handler map) is what lets
|
|
111
|
-
* `errorHandlers` contextually type each handler's `error` param from its key —
|
|
112
|
-
* no annotation needed — while still capturing precise return types for recovery.
|
|
113
|
-
*/
|
|
114
|
-
type HandlerReturns<DomainError extends Tagged> = Partial<Record<DomainError["_tag"], HandlerReturn>>;
|
|
115
|
-
/**
|
|
116
|
-
* The `errorHandlers` field shape for a given return map: one entry per registered
|
|
117
|
-
* tag, whose `error` is the matching domain error (so it autocompletes and types
|
|
118
|
-
* itself) and whose return is the precise, inferred `Returns[Tag]`.
|
|
119
|
-
*/
|
|
120
|
-
type ErrorHandlers<DomainError extends Tagged, Returns> = { [Tag in keyof Returns]: (error: Extract<DomainError, {
|
|
121
|
-
readonly _tag: Tag;
|
|
122
|
-
}>) => Returns[Tag]; };
|
|
123
|
-
/**
|
|
124
|
-
* What the registered handlers recover into loader/action data — from each
|
|
125
|
-
* handler's return type, but only for handlers whose error can actually occur in
|
|
126
|
-
* `E`:
|
|
127
|
-
* - a remapped `ReturnableDataError<B>` recovers as `DataWithResponseInit<B>`;
|
|
128
|
-
* - an `Effect.succeed(value)` recovers as `value`;
|
|
129
|
-
* - throwables, redirects and `Effect.fail(...)` contribute nothing.
|
|
130
|
-
*/
|
|
131
|
-
type RecoverOf<DomainError extends Tagged, Returns, E> = { [Tag in keyof Returns]: [Extract<E, Extract<DomainError, {
|
|
132
|
-
readonly _tag: Tag;
|
|
133
|
-
}>>] 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];
|
|
134
|
-
/**
|
|
135
|
-
* Errors the library deals with on the handler's behalf — so the loader/action/
|
|
136
|
-
* middleware needn't handle them itself:
|
|
137
|
-
* - the app's **declared domain errors** (`DomainError`) — handled by a registered
|
|
138
|
-
* handler, or left to the 500 / auto-respond default;
|
|
139
|
-
* - **library route errors** raised directly via `Respond`;
|
|
140
|
-
* - anything that renders itself via **`HttpServerRespondable`**;
|
|
141
|
-
* - `AlsoHandled` — extra, call-site-specific already-handled errors. Only
|
|
142
|
-
* `makeMiddleware` uses this, to admit the `FailureResponse` its `next()`
|
|
143
|
-
* fails with (a downstream throw, already a finished response — the library
|
|
144
|
-
* re-throws it as-is, so the middleware needn't catch it just to re-raise it).
|
|
145
|
-
*/
|
|
146
|
-
type LibraryHandled<DomainError, AlsoHandled = never> = DomainError | AnyRouteError | HttpServerRespondable.Respondable | AlsoHandled;
|
|
147
|
-
/**
|
|
148
|
-
* What remains in a handler's error channel that the library will NOT handle —
|
|
149
|
-
* i.e. service-specific errors the route consumes that aren't declared domain
|
|
150
|
-
* errors. The loader/action/middleware must handle these itself (catch or map them).
|
|
151
|
-
*/
|
|
152
|
-
type Unhandled<E, DomainError, AlsoHandled = never> = Exclude<E, LibraryHandled<DomainError, AlsoHandled>>;
|
|
153
|
-
/**
|
|
154
|
-
* A readable compile-time diagnostic. It's intersected onto the *effect return type*
|
|
155
|
-
* of `fn` — so the effect's `A`/`E`/`R` still infer directly (naked), while a mismatch
|
|
156
|
-
* reports this message, and the offending types, *at the `makeLoader`/`makeAction`
|
|
157
|
-
* call* instead of the opaque "Expected 2 arguments". Resolves to `unknown` (a no-op
|
|
158
|
-
* intersection) when the effect is fully handled and all its requirements are provided:
|
|
159
|
-
*
|
|
160
|
-
* - **Unhandled error** — the effect can fail with something that isn't a declared
|
|
161
|
-
* domain error, a library route error, or respondable; `unhandledErrors` names them.
|
|
162
|
-
* - **Missing requirement** — the effect requires a service that neither the
|
|
163
|
-
* `runtime` nor the `requestContext` provides; `missingRequirements` names them.
|
|
164
|
-
*
|
|
165
|
-
* The message lives in an *inline* object literal (no named alias) so tsc/tsgo print
|
|
166
|
-
* its text verbatim rather than collapsing it to an alias name.
|
|
167
|
-
*/
|
|
168
|
-
/**
|
|
169
|
-
* Names the unhandled errors for the diagnostic: a tagged error becomes its `_tag`
|
|
170
|
-
* (the string you'd pass to `Effect.catchTag`), anything untagged stays as its type.
|
|
171
|
-
*/
|
|
172
|
-
type NameError<U> = U extends {
|
|
173
|
-
readonly _tag: infer Tag extends string;
|
|
174
|
-
} ? Tag : U;
|
|
175
|
-
/**
|
|
176
|
-
* True exactly when `T` is (or, for a union, includes) `any` — not `unknown`, not
|
|
177
|
-
* `never`, not any other type. An unrelated mistake elsewhere in the effect (a typo,
|
|
178
|
-
* a call with the wrong number of arguments, a genuine syntax error) commonly makes
|
|
179
|
-
* TypeScript infer `any` for `E`/`R` instead of reporting a clean type; running
|
|
180
|
-
* `Diagnose` on that `any` produces a confusing "unhandled errors: any" message that
|
|
181
|
-
* buries the real mistake. Guarding on this and bailing to `unknown` lets tsc's own,
|
|
182
|
-
* on-site error surface instead. (`1 & T` is `any` when `T` is `any` — including when
|
|
183
|
-
* `any` only appears as a union member, since intersection distributes over unions —
|
|
184
|
-
* and `0` is assignable to `any` but not to any concrete type built from `1 & T`.)
|
|
185
|
-
*/
|
|
186
|
-
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
187
|
-
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 : {
|
|
188
|
-
"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.";
|
|
189
|
-
missingRequirements: Exclude<R, Provided>;
|
|
190
|
-
} : {
|
|
191
|
-
"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.";
|
|
192
|
-
unhandledErrors: NameError<Unhandled<E, DomainError, AlsoHandled>>;
|
|
193
|
-
};
|
|
194
|
-
/**
|
|
195
|
-
* A React Router context key holding a per-request effect `Context.Context`. Set
|
|
196
|
-
* it in middleware and pass it to the factory's `requestContext` — the runner
|
|
197
|
-
* reads it on every request and provides its services to the loader/action.
|
|
198
|
-
*
|
|
199
|
-
* It's just a `RouterContext`; create one with React Router's `createContext`:
|
|
200
|
-
*
|
|
201
|
-
* ```ts
|
|
202
|
-
* import { createContext } from "react-router";
|
|
203
|
-
* import { Context } from "effect";
|
|
204
|
-
* import type { RequestContextKey } from "react-router-effect";
|
|
205
|
-
*
|
|
206
|
-
* class RequestContext extends Context.Service<RequestContext, {
|
|
207
|
-
* readonly userId: string;
|
|
208
|
-
* }>()("app/RequestContext") {}
|
|
209
|
-
*
|
|
210
|
-
* export const requestContext: RequestContextKey<RequestContext> = createContext();
|
|
211
|
-
*
|
|
212
|
-
* // middleware:
|
|
213
|
-
* export const middleware: Route.MiddlewareFunction[] = [
|
|
214
|
-
* ({ context, request }, next) => {
|
|
215
|
-
* context.set(requestContext, Context.make(RequestContext, { userId: readUser(request) }));
|
|
216
|
-
* return next();
|
|
217
|
-
* },
|
|
218
|
-
* ];
|
|
219
|
-
* ```
|
|
220
|
-
*/
|
|
221
|
-
type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqServices>>;
|
|
222
|
-
/**
|
|
223
|
-
* Build `makeLoader` / `makeAction` / `makeMiddleware` for an application, wired to
|
|
224
|
-
* its domain errors.
|
|
225
|
-
*
|
|
226
|
-
* Declare the app's **domain errors** as the type argument, then register an
|
|
227
|
-
* *optional* handler per domain error in `errorHandlers`. The handler keys
|
|
228
|
-
* autocomplete to your domain-error tags and each `error` parameter is typed from
|
|
229
|
-
* its key — no annotation needed. It's curried so you can pin the domain errors
|
|
230
|
-
* while the handler return types are still inferred:
|
|
231
|
-
*
|
|
232
|
-
* ```ts
|
|
233
|
-
* type DomainErrors = MyDomainError | DbError | NotAuthorizedError;
|
|
234
|
-
*
|
|
235
|
-
* export const { makeLoader, makeAction, Respond } =
|
|
236
|
-
* makeEffectRouteFactory<DomainErrors>()((Respond) => ({
|
|
237
|
-
* errorHandlers: {
|
|
238
|
-
* // `error` is typed as MyDomainError automatically. throw → error boundary
|
|
239
|
-
* MyDomainError: (error) => Respond.throw({ message: error.message }, 400),
|
|
240
|
-
* // DbError has no handler → falls through to the 500 default.
|
|
241
|
-
* },
|
|
242
|
-
* }));
|
|
243
|
-
* ```
|
|
244
|
-
*
|
|
245
|
-
* The inner config is a **builder** — it receives the library's `Respond` helpers so
|
|
246
|
-
* your handlers can recover/throw with them, and returns the config. `Respond` is
|
|
247
|
-
* also returned from the factory (extended with any `respond` helpers you add), so
|
|
248
|
-
* your app imports a *single* `Respond` — no auto-import ambiguity with a library one.
|
|
249
|
-
*
|
|
250
|
-
* A handler *remaps* the error by returning either a library route error
|
|
251
|
-
* (`Respond.early` to recover; `Respond.throw` / `Respond.redirect` to throw) or an
|
|
252
|
-
* `Effect` (`Effect.succeed(value)` to recover `value`; `Effect.fail(response)` to
|
|
253
|
-
* throw). Recovered values become loader/action data, typed precisely.
|
|
254
|
-
*
|
|
255
|
-
* **Declared domain errors** may be left unhandled: if one implements
|
|
256
|
-
* `HttpServerRespondable` it's rendered automatically, otherwise it falls through
|
|
257
|
-
* to a 500. **Any other error** — a service-specific error the route consumes that
|
|
258
|
-
* isn't a declared domain error — *must* be handled in the loader/action, or
|
|
259
|
-
* `makeLoader`/`makeAction` fails to type-check.
|
|
260
|
-
*
|
|
261
|
-
* Pass a `runtime` (a `ManagedRuntime`, e.g. from `ManagedRuntime.make(AppLayer)`)
|
|
262
|
-
* to provide your app's services once. Loader/action effects may then require those
|
|
263
|
-
* services directly — no per-call `Effect.provide`:
|
|
264
|
-
*
|
|
265
|
-
* ```ts
|
|
266
|
-
* const { makeLoader, makeAction } = makeEffectRouteFactory<DomainErrors>()(() => ({
|
|
267
|
-
* runtime: getAppRuntime(), // provides Database, MyService, ...
|
|
268
|
-
* errorHandlers: { ... },
|
|
269
|
-
* }));
|
|
270
|
-
*
|
|
271
|
-
* // `MyService` is satisfied by the runtime, not provided here:
|
|
272
|
-
* const loader = makeLoader((args: Route.LoaderArgs) =>
|
|
273
|
-
* Effect.gen(function* () {
|
|
274
|
-
* const svc = yield* MyService;
|
|
275
|
-
* return { data: yield* svc.load(args) };
|
|
276
|
-
* }),
|
|
277
|
-
* );
|
|
278
|
-
* ```
|
|
279
|
-
*
|
|
280
|
-
* `makeMiddleware` wraps a `Route.MiddlewareFunction` the same way: write it as an
|
|
281
|
-
* effect, and register the same domain errors/handlers. Pin it to the route's
|
|
282
|
-
* generated `Route.MiddlewareFunction` (a type argument, since middleware has no
|
|
283
|
-
* separate args-only type) to type `args` and `next`'s result from that route; it
|
|
284
|
-
* defaults to React Router's own `MiddlewareFunction`. The `next` handed to the
|
|
285
|
-
* effect is `() => Effect.Effect<Result, FailureResponse>` — call it with `yield*`
|
|
286
|
-
* to run the rest of the chain, and a downstream throw (e.g. a nested loader's
|
|
287
|
-
* redirect) surfaces as a typed failure instead of a rejected promise:
|
|
288
|
-
*
|
|
289
|
-
* ```ts
|
|
290
|
-
* const middleware = makeMiddleware<Route.MiddlewareFunction>((args, next) =>
|
|
291
|
-
* Effect.gen(function* () {
|
|
292
|
-
* const user = yield* getUser(args); // may fail with a declared domain error
|
|
293
|
-
* if (!user) yield* Respond.redirect("/login");
|
|
294
|
-
* return yield* next();
|
|
295
|
-
* }),
|
|
296
|
-
* );
|
|
297
|
-
* ```
|
|
298
|
-
*/
|
|
299
|
-
declare function makeEffectRouteFactory<DomainError extends Tagged = never>(): <const Returns extends HandlerReturns<DomainError> = {}, RServices = never, ReqServices = never, const ExtraRespond extends Record<string, (...args: never[]) => unknown> = {}>(builder: (respond: BaseRespond) => {
|
|
300
|
-
/**
|
|
301
|
-
* An optional handler per declared domain error, keyed by its tag. The keys
|
|
302
|
-
* autocomplete to your domain-error tags and each handler's `error` parameter
|
|
303
|
-
* is typed automatically — no annotation needed. A handler *remaps* the error
|
|
304
|
-
* by returning a library route error (the builder's `Respond.early` / `throw`
|
|
305
|
-
* / `redirect`) or an `Effect` (`Effect.succeed`/`fail`). Omit to register none.
|
|
306
|
-
*/
|
|
307
|
-
errorHandlers?: ErrorHandlers<DomainError, Returns>;
|
|
308
|
-
/**
|
|
309
|
-
* The app runtime that provides services to loader/action effects. When
|
|
310
|
-
* set, effects may require its services (`RServices`, inferred from here)
|
|
311
|
-
* without providing layers, and runs go through `runtime.runPromise`. When
|
|
312
|
-
* omitted, `RServices` is `never` and effects must require nothing.
|
|
313
|
-
*/
|
|
314
|
-
runtime?: ManagedRuntime.ManagedRuntime<RServices, any>;
|
|
315
|
-
/**
|
|
316
|
-
* A React Router context key (a {@link RequestContextKey}) holding a
|
|
317
|
-
* per-request effect `Context.Context`. Middleware sets it for each request;
|
|
318
|
-
* the runner reads `args.context.get(requestContext)` and provides those
|
|
319
|
-
* services to the effect. Loader/action effects may then require
|
|
320
|
-
* `ReqServices` (inferred from here) in addition to the runtime's services.
|
|
321
|
-
*/
|
|
322
|
-
requestContext?: RequestContextKey<ReqServices>;
|
|
323
|
-
/**
|
|
324
|
-
* App-specific `Respond` helpers — merged onto the base `Respond` that the
|
|
325
|
-
* factory returns, so your app imports one `Respond`. Typically opinionated
|
|
326
|
-
* error constructors; annotate their parameters, e.g.
|
|
327
|
-
* `{ formError: (reply: string) => new FormError({ reply }) }`. The base
|
|
328
|
-
* helpers (`early` / `throw` / `redirect`) always win the merge.
|
|
329
|
-
*/
|
|
330
|
-
respond?: ExtraRespond;
|
|
331
|
-
}) => {
|
|
332
|
-
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>>;
|
|
333
|
-
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<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>>;
|
|
334
|
-
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>;
|
|
335
|
-
Respond: {
|
|
336
|
-
early: <D>(value: D, init?: number | ResponseInit) => ReturnableDataError<D>;
|
|
337
|
-
throw: <D>(value: D, init?: number | ResponseInit) => ThrowableDataError<D>;
|
|
338
|
-
redirect: (url: string, init?: number | ResponseInit) => ThrowableRedirectError;
|
|
339
|
-
} & ExtraRespond;
|
|
340
|
-
};
|
|
341
|
-
//#endregion
|
|
342
|
-
export { type AnyRouteError, type ErrorHandler, type RequestContextKey, ReturnableDataError, ThrowableDataError, ThrowableRedirectError, isRouteError, makeEffectRouteFactory };
|
|
1
|
+
import { C as ReturnableDataError, D as WithActionMeta, E as isRouteError, O as actionMetaKey, S as AnyRouteError, T as ThrowableRedirectError, a as RegisteredMeta, b as RouteResult, c as Serialize, d as EffectNext, f as ErrorHandler, i as Register, l as ActionMetaArgs, n as EffectData, o as RegisteredRecover, p as FactoryTypes, r as EffectRouteArgs, s as RouteData, t as BrandData, u as EffectMiddlewareArgs, v as RequestContextKey, w as ThrowableDataError, x as makeEffectRouteFactory, y as RouteHandlers } from "./route-data-MgSnnR33.mjs";
|
|
2
|
+
export { type ActionMetaArgs, type AnyRouteError, type BrandData, type EffectData, type EffectMiddlewareArgs, type EffectNext, type EffectRouteArgs, type ErrorHandler, type FactoryTypes, type Register, type RegisteredMeta, type RegisteredRecover, type RequestContextKey, ReturnableDataError, type RouteData, type RouteHandlers, type RouteResult, type Serialize, ThrowableDataError, ThrowableRedirectError, type WithActionMeta, actionMetaKey, isRouteError, makeEffectRouteFactory };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
|
-
import { Data, Effect } from "effect";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { Context, Data, Effect, Predicate } from "effect";
|
|
2
|
+
import { createContext, data, redirect } from "react-router";
|
|
3
|
+
import { HttpServerRespondable, HttpServerResponse } from "effect/http";
|
|
4
|
+
// src/action-meta.ts
|
|
5
|
+
/** The key that holds the metadata in action data. */
|
|
6
|
+
const actionMetaKey = "__meta";
|
|
7
|
+
const DataWithResponseInitClass = data(void 0).constructor;
|
|
8
|
+
/** True for a `data(...)` wrapper, never for a business field that looks like one. */
|
|
9
|
+
const isDataWithResponseInit = (value) => value instanceof DataWithResponseInitClass;
|
|
10
|
+
/** True for a plain object, which can be copied without losing behavior. */
|
|
11
|
+
const isPlainRecord = (value) => {
|
|
12
|
+
if (!Predicate.isObject(value) || Array.isArray(value)) return false;
|
|
13
|
+
const prototype = Object.getPrototypeOf(value);
|
|
14
|
+
return prototype === Object.prototype || prototype === null;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Attaches `meta` to the action result. A plain record gets a copy with the
|
|
18
|
+
* metadata key; a `data(...)` wrapper gets it on its body and keeps its init.
|
|
19
|
+
* Any other value is returned unchanged.
|
|
20
|
+
*/
|
|
21
|
+
const attachActionMeta = (value, meta) => {
|
|
22
|
+
if (isDataWithResponseInit(value)) {
|
|
23
|
+
if (!isPlainRecord(value.data)) return value;
|
|
24
|
+
return data({
|
|
25
|
+
...value.data,
|
|
26
|
+
[actionMetaKey]: meta
|
|
27
|
+
}, value.init ?? void 0);
|
|
28
|
+
}
|
|
29
|
+
return isPlainRecord(value) ? {
|
|
30
|
+
...value,
|
|
31
|
+
[actionMetaKey]: meta
|
|
32
|
+
} : value;
|
|
33
|
+
};
|
|
34
|
+
// src/errors.ts
|
|
5
35
|
/** Recoverable: short-circuits the loader but returns `data(...)` the component reads. */
|
|
6
36
|
var ReturnableDataError = class extends Data.TaggedError("ReturnableRouteError") {};
|
|
7
37
|
/** Throwable: rejects with `data(...)` → boundary as an `ErrorResponse`. */
|
|
@@ -11,7 +41,7 @@ var ThrowableRedirectError = class extends Data.TaggedError("ThrowableRedirectEr
|
|
|
11
41
|
/**
|
|
12
42
|
* The library's base helpers for raising route errors (`early` / `throw` /
|
|
13
43
|
* `redirect`). This is internal: it isn't exported from the package. The factory
|
|
14
|
-
*
|
|
44
|
+
* passes it to your config builder and returns it (merged with any `respond`
|
|
15
45
|
* extensions) so your app imports a single `Respond`:
|
|
16
46
|
*
|
|
17
47
|
* ```ts
|
|
@@ -24,7 +54,7 @@ var ThrowableRedirectError = class extends Data.TaggedError("ThrowableRedirectEr
|
|
|
24
54
|
* ```
|
|
25
55
|
*/
|
|
26
56
|
const Respond = {
|
|
27
|
-
/** Recover: short-circuit and
|
|
57
|
+
/** Recover: short-circuit and pass `value` to the component as loader/action data. */
|
|
28
58
|
early: (value, init) => new ReturnableDataError({
|
|
29
59
|
data: value,
|
|
30
60
|
init
|
|
@@ -42,11 +72,10 @@ const Respond = {
|
|
|
42
72
|
};
|
|
43
73
|
/** Narrows an unknown value to one of the library's route errors. */
|
|
44
74
|
const isRouteError = (value) => value instanceof ReturnableDataError || value instanceof ThrowableDataError || value instanceof ThrowableRedirectError;
|
|
45
|
-
|
|
46
|
-
//#region src/factory.ts
|
|
75
|
+
// src/factory.ts
|
|
47
76
|
const internalServerError = () => Effect.fail(new Response("Internal Server Error", { status: 500 }));
|
|
48
77
|
/**
|
|
49
|
-
* A raw `FailureResponse`
|
|
78
|
+
* A raw `FailureResponse` - a downstream `next()`'s rejection, in `makeMiddleware` -
|
|
50
79
|
* is already a finished response; re-throwing it as-is (rather than 500ing it) lets
|
|
51
80
|
* a nested loader's redirect/thrown response pass back up through the middleware chain.
|
|
52
81
|
*/
|
|
@@ -58,13 +87,27 @@ const processRouteError = (e) => {
|
|
|
58
87
|
return Effect.fail(redirect(e.url, e.init));
|
|
59
88
|
};
|
|
60
89
|
/**
|
|
90
|
+
* The request store of a factory that configures no `requestContext`. It holds
|
|
91
|
+
* whatever the server adapter and effect middleware put there, and starts empty.
|
|
92
|
+
*/
|
|
93
|
+
const defaultRequestContext = createContext(Context.empty());
|
|
94
|
+
/**
|
|
95
|
+
* The entries of `current` that `started` lacks or holds with another value:
|
|
96
|
+
* what a fiber that began with `started` has provided since.
|
|
97
|
+
*/
|
|
98
|
+
const providedSince = (started, current) => {
|
|
99
|
+
const provided = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const [key, value] of current.mapUnsafe) if (started.mapUnsafe.get(key) !== value) provided.set(key, value);
|
|
101
|
+
return Context.makeUnsafe(provided);
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
61
104
|
* Build `makeLoader` / `makeAction` / `makeMiddleware` for an application, wired to
|
|
62
105
|
* its domain errors.
|
|
63
106
|
*
|
|
64
107
|
* Declare the app's **domain errors** as the type argument, then register an
|
|
65
108
|
* *optional* handler per domain error in `errorHandlers`. The handler keys
|
|
66
109
|
* autocomplete to your domain-error tags and each `error` parameter is typed from
|
|
67
|
-
* its key
|
|
110
|
+
* its key - no annotation needed. It's curried so you can pin the domain errors
|
|
68
111
|
* while the handler return types are still inferred:
|
|
69
112
|
*
|
|
70
113
|
* ```ts
|
|
@@ -80,10 +123,10 @@ const processRouteError = (e) => {
|
|
|
80
123
|
* }));
|
|
81
124
|
* ```
|
|
82
125
|
*
|
|
83
|
-
* The inner config is a **builder**
|
|
126
|
+
* The inner config is a **builder** - it receives the library's `Respond` helpers so
|
|
84
127
|
* your handlers can recover/throw with them, and returns the config. `Respond` is
|
|
85
128
|
* also returned from the factory (extended with any `respond` helpers you add), so
|
|
86
|
-
* your app imports a *single* `Respond`
|
|
129
|
+
* your app imports a *single* `Respond` - no auto-import ambiguity with a library one.
|
|
87
130
|
*
|
|
88
131
|
* A handler *remaps* the error by returning either a library route error
|
|
89
132
|
* (`Respond.early` to recover; `Respond.throw` / `Respond.redirect` to throw) or an
|
|
@@ -92,13 +135,13 @@ const processRouteError = (e) => {
|
|
|
92
135
|
*
|
|
93
136
|
* **Declared domain errors** may be left unhandled: if one implements
|
|
94
137
|
* `HttpServerRespondable` it's rendered automatically, otherwise it falls through
|
|
95
|
-
* to a 500. **Any other error**
|
|
96
|
-
* isn't a declared domain error
|
|
138
|
+
* to a 500. **Any other error** - a service-specific error the route consumes that
|
|
139
|
+
* isn't a declared domain error - *must* be handled in the loader/action, or
|
|
97
140
|
* `makeLoader`/`makeAction` fails to type-check.
|
|
98
141
|
*
|
|
99
142
|
* Pass a `runtime` (a `ManagedRuntime`, e.g. from `ManagedRuntime.make(AppLayer)`)
|
|
100
143
|
* to provide your app's services once. Loader/action effects may then require those
|
|
101
|
-
* services directly
|
|
144
|
+
* services directly - no per-call `Effect.provide`:
|
|
102
145
|
*
|
|
103
146
|
* ```ts
|
|
104
147
|
* const { makeLoader, makeAction } = makeEffectRouteFactory<DomainErrors>()(() => ({
|
|
@@ -120,9 +163,9 @@ const processRouteError = (e) => {
|
|
|
120
163
|
* generated `Route.MiddlewareFunction` (a type argument, since middleware has no
|
|
121
164
|
* separate args-only type) to type `args` and `next`'s result from that route; it
|
|
122
165
|
* defaults to React Router's own `MiddlewareFunction`. The `next` handed to the
|
|
123
|
-
* effect is `() => Effect.Effect<Result, FailureResponse>`
|
|
166
|
+
* effect is `() => Effect.Effect<Result, FailureResponse>` - call it with `yield*`
|
|
124
167
|
* to run the rest of the chain, and a downstream throw (e.g. a nested loader's
|
|
125
|
-
* redirect)
|
|
168
|
+
* redirect) becomes a typed failure instead of a rejected promise:
|
|
126
169
|
*
|
|
127
170
|
* ```ts
|
|
128
171
|
* const middleware = makeMiddleware<Route.MiddlewareFunction>((args, next) =>
|
|
@@ -138,7 +181,7 @@ function makeEffectRouteFactory() {
|
|
|
138
181
|
return function defineErrorHandlers(builder) {
|
|
139
182
|
const config = builder(Respond);
|
|
140
183
|
const runtime = config.runtime;
|
|
141
|
-
const
|
|
184
|
+
const storeKey = config.requestContext ?? defaultRequestContext;
|
|
142
185
|
const Respond$1 = {
|
|
143
186
|
...config.respond,
|
|
144
187
|
...Respond
|
|
@@ -150,9 +193,9 @@ function makeEffectRouteFactory() {
|
|
|
150
193
|
* (registered domain errors, library route errors, respondables, or the 500
|
|
151
194
|
* default), provides the per-request context, and executes against the
|
|
152
195
|
* configured runtime. Shared by `makeLoader`/`makeAction` and `makeMiddleware`
|
|
153
|
-
*
|
|
196
|
+
* - they differ only in how they build the effect and what `context` they read.
|
|
154
197
|
*/
|
|
155
|
-
function runProgram(effect, context) {
|
|
198
|
+
function runProgram(effect, context, signal) {
|
|
156
199
|
const program = effect.pipe(Effect.catchIf((_e) => true, (e) => {
|
|
157
200
|
if (isUserError(e)) {
|
|
158
201
|
const out = userHandlers[e._tag](e);
|
|
@@ -163,12 +206,13 @@ function makeEffectRouteFactory() {
|
|
|
163
206
|
if (isFailureResponse(e)) return Effect.fail(e);
|
|
164
207
|
return internalServerError();
|
|
165
208
|
}));
|
|
166
|
-
const
|
|
167
|
-
|
|
209
|
+
const store = readRequestStore(context);
|
|
210
|
+
const provided = store === void 0 ? program : Effect.provideContext(program, store);
|
|
211
|
+
return runtime ? runtime.runPromise(provided, { signal }) : Effect.runPromise(provided, { signal });
|
|
168
212
|
}
|
|
169
213
|
function makeLoaderOrAction(fn) {
|
|
170
214
|
const run = fn;
|
|
171
|
-
return (args) => runProgram(run(args), args.context);
|
|
215
|
+
return (args) => runProgram(run(args), args.context, args.request.signal);
|
|
172
216
|
}
|
|
173
217
|
/**
|
|
174
218
|
* Wraps a middleware function written as an effect. Pin it to the route's generated
|
|
@@ -176,13 +220,13 @@ function makeEffectRouteFactory() {
|
|
|
176
220
|
* args-only type) to type `args`, `next` and the result from that route; it defaults
|
|
177
221
|
* to React Router's own `MiddlewareFunction`.
|
|
178
222
|
*
|
|
179
|
-
* The errors the library deals with for a loader/action it deals with here too
|
|
180
|
-
* declared domain errors, library route errors and respondables
|
|
223
|
+
* The errors the library deals with for a loader/action it deals with here too -
|
|
224
|
+
* declared domain errors, library route errors and respondables - plus the
|
|
181
225
|
* `FailureResponse` `next()` fails with. Anything else must be handled in the effect,
|
|
182
226
|
* as must any service neither the `runtime` nor the `requestContext` provides.
|
|
183
227
|
*
|
|
184
|
-
* The wrapper is typed as the route's own middleware
|
|
185
|
-
* Promise<Result | undefined>`
|
|
228
|
+
* The wrapper is typed as the route's own middleware - `(args, next) =>
|
|
229
|
+
* Promise<Result | undefined>` - so it drops straight into a
|
|
186
230
|
* `Route.MiddlewareFunction[]`. A handler that recovers (`Respond.early`) resolves
|
|
187
231
|
* with `data(...)`, which react-router's server middleware pipeline turns into the
|
|
188
232
|
* short-circuit `Response`.
|
|
@@ -190,21 +234,65 @@ function makeEffectRouteFactory() {
|
|
|
190
234
|
function makeMiddleware(fn) {
|
|
191
235
|
const run = fn;
|
|
192
236
|
return (args, next) => {
|
|
193
|
-
const
|
|
237
|
+
const { context, request } = args;
|
|
238
|
+
const effectNext = makeEffectNext(context, next);
|
|
239
|
+
return runProgram(run(args, effectNext), context, request.signal);
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Reads the request's effect context from the router context. A test that
|
|
244
|
+
* passes args without a router context gets none, as before.
|
|
245
|
+
*/
|
|
246
|
+
function readRequestStore(context) {
|
|
247
|
+
return typeof context?.get === "function" ? context.get(storeKey) : void 0;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Wraps React Router's `next` as an effect. Before it runs the rest of the
|
|
251
|
+
* chain, it adds to the request's effect context what the calling fiber
|
|
252
|
+
* provides beyond the context the middleware started with: services it
|
|
253
|
+
* provides around `next()`, log annotations, and its current span. Updates
|
|
254
|
+
* the middleware made with `context.set` stay. A downstream rejection is
|
|
255
|
+
* already a finished response, so it fails with that response as-is.
|
|
256
|
+
*/
|
|
257
|
+
function makeEffectNext(context, next) {
|
|
258
|
+
const started = readRequestStore(context);
|
|
259
|
+
return () => Effect.withFiber((fiber) => {
|
|
260
|
+
if (started !== void 0) context.set(storeKey, Context.merge(context.get(storeKey), providedSince(started, fiber.context)));
|
|
261
|
+
return Effect.tryPromise({
|
|
194
262
|
try: next,
|
|
195
263
|
catch: (e) => e
|
|
196
264
|
});
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/** Runs a loader or action's result: an Effect through the error model, anything else as-is. */
|
|
268
|
+
function runResult(result, args) {
|
|
269
|
+
return Effect.isEffect(result) ? runProgram(result, args.context, args.request.signal) : result;
|
|
200
270
|
}
|
|
271
|
+
const actionMeta = config.actionMeta;
|
|
201
272
|
return {
|
|
202
273
|
makeLoader: makeLoaderOrAction,
|
|
203
274
|
makeAction: makeLoaderOrAction,
|
|
204
275
|
makeMiddleware,
|
|
205
|
-
Respond: Respond$1
|
|
276
|
+
Respond: Respond$1,
|
|
277
|
+
/** The route runner the server adapter applies to every route module. */
|
|
278
|
+
handlers: {
|
|
279
|
+
loader: (fn) => async (args) => runResult(fn(args), args),
|
|
280
|
+
action: (fn) => async (args) => {
|
|
281
|
+
const result = await runResult(fn(args), args);
|
|
282
|
+
return actionMeta === void 0 ? result : attachActionMeta(result, actionMeta(args));
|
|
283
|
+
},
|
|
284
|
+
middleware: (fn) => async (args, next) => {
|
|
285
|
+
const routeArgs = args;
|
|
286
|
+
return runResult(fn({
|
|
287
|
+
...args,
|
|
288
|
+
next: makeEffectNext(routeArgs.context, next)
|
|
289
|
+
}, next), routeArgs);
|
|
290
|
+
},
|
|
291
|
+
requestContext: storeKey
|
|
292
|
+
},
|
|
293
|
+
/** Type-only: what the adapter's route check and `Register` read. */
|
|
294
|
+
"~types": void 0
|
|
206
295
|
};
|
|
207
296
|
};
|
|
208
297
|
}
|
|
209
|
-
|
|
210
|
-
export { ReturnableDataError, ThrowableDataError, ThrowableRedirectError, isRouteError, makeEffectRouteFactory };
|
|
298
|
+
export { ReturnableDataError, ThrowableDataError, ThrowableRedirectError, actionMetaKey, isRouteError, makeEffectRouteFactory };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
export { __exportAll as t };
|