react-router-effect 2.1.0 → 2.3.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 +373 -117
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +160 -23
- package/dist/{route-data-MgSnnR33.d.mts → route-data-ByiuG-Yb.d.mts} +85 -26
- package/dist/server.d.mts +141 -75
- package/dist/server.mjs +286 -140
- package/dist/vite.d.mts +35 -14
- package/dist/vite.mjs +47 -13
- package/package.json +12 -7
- package/src/assets.ts +212 -0
- package/src/check.ts +9 -1
- package/src/defer.ts +179 -0
- package/src/factory.ts +161 -42
- package/src/index.ts +7 -3
- package/src/route-data.ts +9 -9
- package/src/{action-meta.ts → route-meta.ts} +19 -17
- package/src/server-entry.ts +2 -1
- package/src/server.ts +307 -295
- package/src/vite.ts +109 -32
package/src/factory.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { Context, Effect, type ManagedRuntime } from "effect";
|
|
1
|
+
import { Context, Effect, type ManagedRuntime, Predicate } from "effect";
|
|
2
2
|
import { HttpServerRespondable, HttpServerResponse } from "effect/http";
|
|
3
3
|
import {
|
|
4
4
|
createContext,
|
|
5
5
|
data,
|
|
6
6
|
redirect,
|
|
7
7
|
type ActionFunctionArgs,
|
|
8
|
+
type ErrorResponse,
|
|
8
9
|
type UNSAFE_DataWithResponseInit as DataWithResponseInit,
|
|
9
10
|
type LoaderFunctionArgs,
|
|
10
11
|
type MiddlewareFunction,
|
|
@@ -12,7 +13,15 @@ import {
|
|
|
12
13
|
type RouterContextProvider,
|
|
13
14
|
} from "react-router";
|
|
14
15
|
|
|
15
|
-
import {
|
|
16
|
+
import { attachRouteMeta, type WithRouteMeta } from "./route-meta.ts";
|
|
17
|
+
import {
|
|
18
|
+
deferFailureResponse,
|
|
19
|
+
deferRouteError,
|
|
20
|
+
forkDeferred,
|
|
21
|
+
withDeferScope,
|
|
22
|
+
type DeferDiagnose,
|
|
23
|
+
type DeferredPromise,
|
|
24
|
+
} from "./defer.ts";
|
|
16
25
|
import {
|
|
17
26
|
isRouteError,
|
|
18
27
|
Respond as baseRespond,
|
|
@@ -124,6 +133,40 @@ export type RecoverOf<DomainError extends Tagged, Returns, E> = {
|
|
|
124
133
|
| (Returns[Tag] extends Effect.Effect<infer SuccessValue, any, any> ? SuccessValue : never);
|
|
125
134
|
}[keyof Returns];
|
|
126
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The errors the factory's `defer` handles in deferred work: the declared
|
|
138
|
+
* domain errors, and `Respond.early` / `Respond.throw`. A redirect is not one
|
|
139
|
+
* of them: it cannot happen after the response started.
|
|
140
|
+
*
|
|
141
|
+
* @stability unstable
|
|
142
|
+
*/
|
|
143
|
+
export type DeferHandled<DomainError> =
|
|
144
|
+
| DomainError
|
|
145
|
+
| ReturnableDataError<unknown>
|
|
146
|
+
| ThrowableDataError<unknown>;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* What a deferred promise resolves with when the work fails with `E`: the
|
|
150
|
+
* value of a `Respond.early` raised directly, and what the registered
|
|
151
|
+
* handlers recover. Unlike loader data, a recovered `Respond.early` gives its
|
|
152
|
+
* value without the `data(...)` wrapper: a promise has no response to set.
|
|
153
|
+
*
|
|
154
|
+
* @stability unstable
|
|
155
|
+
*/
|
|
156
|
+
export type DeferRecoverOf<DomainError extends Tagged, Returns, E> =
|
|
157
|
+
| ReturnableBodyOf<E>
|
|
158
|
+
| {
|
|
159
|
+
[Tag in keyof Returns]: [Extract<E, Extract<DomainError, { readonly _tag: Tag }>>] extends [
|
|
160
|
+
never,
|
|
161
|
+
]
|
|
162
|
+
? never
|
|
163
|
+
:
|
|
164
|
+
| (Returns[Tag] extends ReturnableDataError<infer B> ? B : never)
|
|
165
|
+
| (Returns[Tag] extends Effect.Effect<infer SuccessValue, any, any>
|
|
166
|
+
? SuccessValue
|
|
167
|
+
: never);
|
|
168
|
+
}[keyof Returns];
|
|
169
|
+
|
|
127
170
|
/**
|
|
128
171
|
* Errors the library deals with on the handler's behalf - so the loader/action/
|
|
129
172
|
* middleware needn't handle them itself:
|
|
@@ -244,6 +287,12 @@ const processRouteError = (
|
|
|
244
287
|
* it in middleware and pass it to the factory's `requestContext` - the runner
|
|
245
288
|
* reads it on every request and provides its services to the loader/action.
|
|
246
289
|
*
|
|
290
|
+
* With the server adapter, do not set it to a new context. The adapter sets it
|
|
291
|
+
* to the request fiber's context before React Router runs, and a new context
|
|
292
|
+
* replaces those services, such as `HttpServerRequest` and the server span.
|
|
293
|
+
* Provide request services with a router middleware on `ReactRouter.routes`.
|
|
294
|
+
* The middleware below is for an app without the adapter.
|
|
295
|
+
*
|
|
247
296
|
* It's just a `RouterContext`; create one with React Router's `createContext`:
|
|
248
297
|
*
|
|
249
298
|
* ```ts
|
|
@@ -257,7 +306,7 @@ const processRouteError = (
|
|
|
257
306
|
*
|
|
258
307
|
* export const requestContext: RequestContextKey<RequestContext> = createContext();
|
|
259
308
|
*
|
|
260
|
-
* // middleware:
|
|
309
|
+
* // middleware, in an app without the server adapter:
|
|
261
310
|
* export const middleware: Route.MiddlewareFunction[] = [
|
|
262
311
|
* ({ context, request }, next) => {
|
|
263
312
|
* context.set(requestContext, Context.make(RequestContext, { userId: readUser(request) }));
|
|
@@ -274,8 +323,8 @@ export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqSe
|
|
|
274
323
|
*/
|
|
275
324
|
const defaultRequestContext: RequestContextKey<never> = createContext(Context.empty());
|
|
276
325
|
|
|
277
|
-
/** What `
|
|
278
|
-
export interface
|
|
326
|
+
/** What `routeMeta` reads: the request and the router context of the loader or action. */
|
|
327
|
+
export interface RouteMetaArgs {
|
|
279
328
|
readonly request: Request;
|
|
280
329
|
readonly context: Readonly<RouterContextProvider>;
|
|
281
330
|
}
|
|
@@ -488,13 +537,14 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
488
537
|
*/
|
|
489
538
|
respond?: ExtraRespond;
|
|
490
539
|
/**
|
|
491
|
-
* Request-scoped metadata to attach to every
|
|
492
|
-
* read after the
|
|
493
|
-
* action it runs, including plain functions
|
|
494
|
-
* thrown result gets none. Only
|
|
495
|
-
* it; other values are returned
|
|
540
|
+
* Request-scoped metadata to attach to the data of every loader and action
|
|
541
|
+
* under `__meta`, read after the function resolves. The server adapter
|
|
542
|
+
* attaches it to every loader and action it runs, including plain functions
|
|
543
|
+
* and `makeLoader` / `makeAction` wrappers. A thrown result gets none. Only
|
|
544
|
+
* plain records and `data(...)` bodies include it; other values are returned
|
|
545
|
+
* unchanged.
|
|
496
546
|
*/
|
|
497
|
-
|
|
547
|
+
routeMeta?: (args: RouteMetaArgs) => Meta;
|
|
498
548
|
},
|
|
499
549
|
) {
|
|
500
550
|
const config = builder(baseRespond);
|
|
@@ -511,11 +561,10 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
511
561
|
// Defaults to an empty map when `errorHandlers` is omitted.
|
|
512
562
|
const userHandlers = (config.errorHandlers ?? {}) as unknown as Record<
|
|
513
563
|
string,
|
|
514
|
-
ErrorHandler<
|
|
564
|
+
ErrorHandler<Tagged>
|
|
515
565
|
>;
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
typeof e === "object" && e !== null && "_tag" in e && (e as Tagged)._tag in userHandlers;
|
|
566
|
+
/** The tags of the registered domain errors. */
|
|
567
|
+
const userTags = Object.keys(userHandlers);
|
|
519
568
|
|
|
520
569
|
/**
|
|
521
570
|
* Runs a handler's effect to completion: dispatches its whole error channel
|
|
@@ -532,7 +581,10 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
532
581
|
// The internal channel is deliberately loose (`unknown` success); the caller
|
|
533
582
|
// restores the precise resolved type (computed from `E` and the handler map).
|
|
534
583
|
// Sound at runtime - the values produced are exactly those types.
|
|
535
|
-
|
|
584
|
+
// Deferred work forks into a scope of this run, closed when the request
|
|
585
|
+
// aborts or the effect fails. It is inside the dispatch, so a failure that
|
|
586
|
+
// a handler recovers also interrupts the work: its promises are not sent.
|
|
587
|
+
const program = withDeferScope(effect, signal).pipe(
|
|
536
588
|
// Catch the full error channel and dispatch. The refinement is `e is E`
|
|
537
589
|
// (provably ⊆ E, and a *refinement* not a bare predicate - the predicate
|
|
538
590
|
// overload crashes tsc over a generic `E`). A declared domain error with no
|
|
@@ -542,9 +594,11 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
542
594
|
(e): Effect.Effect<unknown, FailureResponse> => {
|
|
543
595
|
// Registered domain error → remap. A library-error return is processed
|
|
544
596
|
// by the internal dispatch; an `Effect` return is used as-is.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
597
|
+
for (const tag of userTags) {
|
|
598
|
+
if (Predicate.isTagged(e, tag)) {
|
|
599
|
+
const out = userHandlers[tag](e);
|
|
600
|
+
return isRouteError(out) ? processRouteError(out) : out;
|
|
601
|
+
}
|
|
548
602
|
}
|
|
549
603
|
// A library route error raised directly in the handler → recover/throw.
|
|
550
604
|
if (isRouteError(e)) return processRouteError(e);
|
|
@@ -570,6 +624,7 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
570
624
|
// `provideContext` removes `ReqServices`, and when no `requestContext` is
|
|
571
625
|
// configured `ReqServices` is `never` (nothing removed).
|
|
572
626
|
const store = readRequestStore(context);
|
|
627
|
+
// oxlint-disable-next-line effecttsgo/unsafe-effect-type-assertion -- the cast is sound, as the comment above explains
|
|
573
628
|
const provided = (
|
|
574
629
|
store === undefined ? program : Effect.provideContext(program, store)
|
|
575
630
|
) as Effect.Effect<unknown, FailureResponse, RServices>;
|
|
@@ -580,7 +635,8 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
580
635
|
// signal and discards its result, so the interrupt rejection is never rendered.
|
|
581
636
|
return runtime
|
|
582
637
|
? runtime.runPromise(provided, { signal })
|
|
583
|
-
:
|
|
638
|
+
: // oxlint-disable-next-line effecttsgo/unsafe-effect-type-assertion -- with no runtime `RServices` is `never`
|
|
639
|
+
Effect.runPromise(provided as Effect.Effect<unknown, FailureResponse>, { signal });
|
|
584
640
|
}
|
|
585
641
|
|
|
586
642
|
function makeLoaderOrAction<Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E, R>(
|
|
@@ -593,15 +649,18 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
593
649
|
fn: (
|
|
594
650
|
args: Args,
|
|
595
651
|
) => Effect.Effect<A, E, R> & Diagnose<E, R, DomainError, RServices | ReqServices>,
|
|
596
|
-
): (
|
|
652
|
+
): (
|
|
653
|
+
args: Args,
|
|
654
|
+
) => Promise<WithRouteMeta<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>, Meta>> {
|
|
597
655
|
// The diagnostic is irrelevant inside the body (it's `unknown` for any valid
|
|
598
656
|
// call); treat `fn` as the plain effect-returning function it is.
|
|
599
657
|
const run = fn as unknown as (
|
|
600
658
|
args: Args,
|
|
601
659
|
) => Effect.Effect<unknown, E, RServices | ReqServices>;
|
|
660
|
+
// The server adapter's `handlers` attach the metadata to what this resolves with.
|
|
602
661
|
return (args: Args) =>
|
|
603
662
|
runProgram(run(args), args.context, args.request.signal) as Promise<
|
|
604
|
-
A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>
|
|
663
|
+
WithRouteMeta<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>, Meta>
|
|
605
664
|
>;
|
|
606
665
|
}
|
|
607
666
|
|
|
@@ -705,6 +764,7 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
705
764
|
function runResult(result: unknown, args: RouteArgs): unknown {
|
|
706
765
|
return Effect.isEffect(result)
|
|
707
766
|
? runProgram(
|
|
767
|
+
// oxlint-disable-next-line effecttsgo/any-unknown-in-error-context, effecttsgo/unsafe-effect-type-assertion -- a route function's Effect is only known to be an Effect here; `runProgram` dispatches its whole error channel
|
|
708
768
|
result as Effect.Effect<unknown, unknown, RServices | ReqServices>,
|
|
709
769
|
args.context,
|
|
710
770
|
args.request.signal,
|
|
@@ -712,7 +772,80 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
712
772
|
: result;
|
|
713
773
|
}
|
|
714
774
|
|
|
715
|
-
|
|
775
|
+
/**
|
|
776
|
+
* A failure of deferred work, through the error handlers: a recovered
|
|
777
|
+
* value resolves the promise, a thrown response rejects it with an
|
|
778
|
+
* `ErrorResponse`. Anything else, such as a domain error with no handler,
|
|
779
|
+
* is a defect: the promise rejects with no server details.
|
|
780
|
+
*/
|
|
781
|
+
function dispatchDeferred(e: unknown): Effect.Effect<unknown, ErrorResponse> {
|
|
782
|
+
for (const tag of userTags) {
|
|
783
|
+
if (Predicate.isTagged(e, tag)) {
|
|
784
|
+
const out = userHandlers[tag](e);
|
|
785
|
+
return isRouteError(out)
|
|
786
|
+
? deferRouteError(out)
|
|
787
|
+
: out.pipe(Effect.catch(deferFailureResponse));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return isRouteError(e) ? deferRouteError(e) : Effect.die(e);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Starts an Effect without waiting for it, and gives a promise of its
|
|
795
|
+
* result for loader data. React Router streams the promise to `<Await>`.
|
|
796
|
+
*
|
|
797
|
+
* The work gets the services and the span of the Effect that defers it.
|
|
798
|
+
* It keeps running after the loader returns, and is interrupted when the
|
|
799
|
+
* request aborts or the loader fails. A client-side navigation away does
|
|
800
|
+
* not abort the request, so the work then runs until it ends or the stream
|
|
801
|
+
* times out.
|
|
802
|
+
*
|
|
803
|
+
* The work may fail only with the factory's domain errors,
|
|
804
|
+
* `Respond.early` and `Respond.throw`. The error handlers turn them into
|
|
805
|
+
* the promise's value, or an `ErrorResponse` for `useAsyncError`, as they
|
|
806
|
+
* do for a loader. A handler that redirects, a domain error with no
|
|
807
|
+
* handler, a defect or an interrupt rejects the promise with an error that
|
|
808
|
+
* holds no server details, and the server logs the full cause.
|
|
809
|
+
*
|
|
810
|
+
* ```ts
|
|
811
|
+
* export const { Respond, defer } = routeFactory;
|
|
812
|
+
*
|
|
813
|
+
* export const loader = Effect.fn("loader")(function* () {
|
|
814
|
+
* const user = yield* Users.current;
|
|
815
|
+
* return {
|
|
816
|
+
* user,
|
|
817
|
+
* reviews: yield* defer(Reviews.list(user.id)),
|
|
818
|
+
* stats: yield* Stats.load.pipe(
|
|
819
|
+
* Effect.catchTag("StatsUnavailable", () => Effect.succeed(null)),
|
|
820
|
+
* defer,
|
|
821
|
+
* ),
|
|
822
|
+
* };
|
|
823
|
+
* });
|
|
824
|
+
* ```
|
|
825
|
+
*
|
|
826
|
+
* @stability unstable
|
|
827
|
+
*/
|
|
828
|
+
function defer<A, E, R>(
|
|
829
|
+
self: Effect.Effect<A, E, R> & DeferDiagnose<E, DeferHandled<DomainError>>,
|
|
830
|
+
): Effect.Effect<DeferredPromise<A | DeferRecoverOf<DomainError, Returns, E>>, never, R> {
|
|
831
|
+
const work: Effect.Effect<unknown, ErrorResponse, R> = self.pipe(
|
|
832
|
+
Effect.catch(dispatchDeferred),
|
|
833
|
+
);
|
|
834
|
+
// The dispatch resolves with what `DeferRecoverOf` computes from the handlers.
|
|
835
|
+
return forkDeferred(work) as Effect.Effect<
|
|
836
|
+
DeferredPromise<A | DeferRecoverOf<DomainError, Returns, E>>,
|
|
837
|
+
never,
|
|
838
|
+
R
|
|
839
|
+
>;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const routeMeta = config.routeMeta;
|
|
843
|
+
|
|
844
|
+
/** Runs a loader or action's result and attaches the route metadata to its data. */
|
|
845
|
+
async function withRouteMeta(result: unknown, args: RouteArgs): Promise<unknown> {
|
|
846
|
+
const resolved = await runResult(result, args);
|
|
847
|
+
return routeMeta === undefined ? resolved : attachRouteMeta(resolved, routeMeta(args));
|
|
848
|
+
}
|
|
716
849
|
|
|
717
850
|
// React Router calls every route function with args that include at least
|
|
718
851
|
// `request` and `context`, so each wrapper reads them through `RouteArgs`.
|
|
@@ -720,13 +853,9 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
720
853
|
// or recovered data, or the awaited plain result. `RouteResult` is its type.
|
|
721
854
|
const handlers: RouteHandlers = {
|
|
722
855
|
loader: (fn) => async (args) =>
|
|
723
|
-
|
|
724
|
-
action: (fn) => async (args) =>
|
|
725
|
-
|
|
726
|
-
return (
|
|
727
|
-
actionMeta === undefined ? result : attachActionMeta(result, actionMeta(args))
|
|
728
|
-
) as RouteResult<ReturnType<typeof fn>>;
|
|
729
|
-
},
|
|
856
|
+
(await withRouteMeta(fn(args), args)) as RouteResult<ReturnType<typeof fn>>,
|
|
857
|
+
action: (fn) => async (args) =>
|
|
858
|
+
(await withRouteMeta(fn(args), args)) as RouteResult<ReturnType<typeof fn>>,
|
|
730
859
|
middleware: (fn) => async (args, next) => {
|
|
731
860
|
const routeArgs: RouteArgs = args;
|
|
732
861
|
// The runner supplies `next` as an effect in place of the one `Args`
|
|
@@ -742,20 +871,10 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
742
871
|
|
|
743
872
|
return {
|
|
744
873
|
makeLoader: makeLoaderOrAction,
|
|
745
|
-
makeAction: makeLoaderOrAction
|
|
746
|
-
Args extends LoaderFunctionArgs | ActionFunctionArgs,
|
|
747
|
-
A,
|
|
748
|
-
E,
|
|
749
|
-
R,
|
|
750
|
-
>(
|
|
751
|
-
fn: (
|
|
752
|
-
args: Args,
|
|
753
|
-
) => Effect.Effect<A, E, R> & Diagnose<E, R, DomainError, RServices | ReqServices>,
|
|
754
|
-
) => (
|
|
755
|
-
args: Args,
|
|
756
|
-
) => Promise<WithActionMeta<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>, Meta>>,
|
|
874
|
+
makeAction: makeLoaderOrAction,
|
|
757
875
|
makeMiddleware,
|
|
758
876
|
Respond,
|
|
877
|
+
defer,
|
|
759
878
|
/** The route runner the server adapter applies to every route module. */
|
|
760
879
|
handlers,
|
|
761
880
|
/** Type-only: what the adapter's route check and `Register` read. */
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
1
|
+
export { routeMetaKey } from "./route-meta.ts";
|
|
2
|
+
export type { WithRouteMeta } from "./route-meta.ts";
|
|
3
|
+
export { deferredFailureMessage } from "./defer.ts";
|
|
4
|
+
export type { DeferDiagnose, DeferredPromise } from "./defer.ts";
|
|
3
5
|
export {
|
|
4
6
|
isRouteError,
|
|
5
7
|
ReturnableDataError,
|
|
@@ -9,7 +11,9 @@ export {
|
|
|
9
11
|
export type { AnyRouteError } from "./errors.ts";
|
|
10
12
|
export { makeEffectRouteFactory } from "./factory.ts";
|
|
11
13
|
export type {
|
|
12
|
-
|
|
14
|
+
DeferHandled,
|
|
15
|
+
DeferRecoverOf,
|
|
16
|
+
RouteMetaArgs,
|
|
13
17
|
EffectMiddlewareArgs,
|
|
14
18
|
EffectNext,
|
|
15
19
|
ErrorHandler,
|
package/src/route-data.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Effect } from "effect";
|
|
2
2
|
import type { UNSAFE_DataWithResponseInit, useLoaderData } from "react-router";
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { WithRouteMeta } from "./route-meta.ts";
|
|
5
5
|
import type { ReturnableDataError } from "./errors.ts";
|
|
6
6
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
@@ -28,8 +28,8 @@ import type { ReturnableDataError } from "./errors.ts";
|
|
|
28
28
|
* interface Register {
|
|
29
29
|
* // Each error tag a handler recovers, and the data it recovers into.
|
|
30
30
|
* recover: { FormValidationError: { reply: SubmissionResponse } };
|
|
31
|
-
* // What `
|
|
32
|
-
*
|
|
31
|
+
* // What `routeMeta` returns.
|
|
32
|
+
* routeMeta: { realtimeEventIds: ReadonlyArray<string> };
|
|
33
33
|
* }
|
|
34
34
|
* }
|
|
35
35
|
* ```
|
|
@@ -39,8 +39,8 @@ export interface Register {}
|
|
|
39
39
|
/** The registered recover map: error tag to recovered data. */
|
|
40
40
|
export type RegisteredRecover = Register extends { readonly recover: infer Recover } ? Recover : {};
|
|
41
41
|
|
|
42
|
-
/** The registered
|
|
43
|
-
export type RegisteredMeta = Register extends { readonly
|
|
42
|
+
/** The registered route metadata, or `never` when there is none. */
|
|
43
|
+
export type RegisteredMeta = Register extends { readonly routeMeta: infer Meta } ? Meta : never;
|
|
44
44
|
|
|
45
45
|
/** What a failure of type `E` recovers into: `Respond.early` bodies and registered handlers. */
|
|
46
46
|
type Recovered<E> =
|
|
@@ -59,9 +59,9 @@ export type Serialize<T> = ReturnType<typeof useLoaderData<() => Promise<T>>>;
|
|
|
59
59
|
/**
|
|
60
60
|
* The data an Effect that succeeds with `A` and fails with `E` gives the
|
|
61
61
|
* component: its success and what its failures recover into, serialized as
|
|
62
|
-
* React Router serializes them, with the registered
|
|
62
|
+
* React Router serializes them, with the registered route metadata.
|
|
63
63
|
*/
|
|
64
|
-
export type EffectData<A, E> =
|
|
64
|
+
export type EffectData<A, E> = WithRouteMeta<Serialize<A | Recovered<E>>, RegisteredMeta>;
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* The route data of a loader or action, for React Router's hooks:
|
|
@@ -108,10 +108,10 @@ type Unwrapped<A> = A extends Response
|
|
|
108
108
|
* declarations. `test/consumer` checks the built package for that.
|
|
109
109
|
*/
|
|
110
110
|
export type BrandData<A, E> = [A] extends [never]
|
|
111
|
-
?
|
|
111
|
+
? WithRouteMeta<Recovered<E>, RegisteredMeta>
|
|
112
112
|
: [A] extends [void]
|
|
113
113
|
? unknown
|
|
114
|
-
:
|
|
114
|
+
: WithRouteMeta<Unwrapped<A> | Recovered<E>, RegisteredMeta>;
|
|
115
115
|
|
|
116
116
|
declare module "effect/Effect" {
|
|
117
117
|
// React Router reads loader and action data through this brand, so an
|
|
@@ -2,11 +2,11 @@ import { Predicate } from "effect";
|
|
|
2
2
|
import { data, type UNSAFE_DataWithResponseInit as DataWithResponseInit } from "react-router";
|
|
3
3
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
|
-
//
|
|
5
|
+
// Route metadata: request-scoped facts attached to every loader and action's data.
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
|
|
8
|
-
/** The key that holds the metadata in action data. */
|
|
9
|
-
export const
|
|
8
|
+
/** The key that holds the metadata in loader and action data. */
|
|
9
|
+
export const routeMetaKey = "__meta";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Adds optional metadata to a record. Only records get metadata, because a
|
|
@@ -14,27 +14,29 @@ export const actionMetaKey = "__meta";
|
|
|
14
14
|
* values (strings, arrays, `Response`s) have nowhere to put it.
|
|
15
15
|
*/
|
|
16
16
|
type RecordWithMeta<T, Meta> =
|
|
17
|
-
T extends Record<string, unknown>
|
|
18
|
-
? Omit<T, typeof actionMetaKey> & { [actionMetaKey]?: Meta }
|
|
19
|
-
: T;
|
|
17
|
+
T extends Record<string, unknown> ? Omit<T, typeof routeMetaKey> & { [routeMetaKey]?: Meta } : T;
|
|
20
18
|
|
|
21
19
|
/**
|
|
22
|
-
* The type of action data after metadata is attached. A `data(...)` wrapper
|
|
20
|
+
* The type of loader or action data after metadata is attached. A `data(...)` wrapper
|
|
23
21
|
* gets metadata on its body. With no configured metadata (`Meta` is `never`)
|
|
24
22
|
* the data is unchanged.
|
|
25
23
|
*/
|
|
26
|
-
export type
|
|
24
|
+
export type WithRouteMeta<T, Meta> = [Meta] extends [never]
|
|
27
25
|
? T
|
|
28
26
|
: T extends DataWithResponseInit<infer Body>
|
|
29
27
|
? DataWithResponseInit<RecordWithMeta<Body, Meta>>
|
|
30
28
|
: RecordWithMeta<T, Meta>;
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
/**
|
|
31
|
+
* True for a `data(...)` wrapper, by the same test React Router applies. A
|
|
32
|
+
* class test fails when the app and the adapter load different copies of
|
|
33
|
+
* React Router, as under the Vite dev server.
|
|
34
|
+
*/
|
|
36
35
|
const isDataWithResponseInit = (value: unknown): value is DataWithResponseInit<unknown> =>
|
|
37
|
-
value
|
|
36
|
+
Predicate.hasProperty(value, "type") &&
|
|
37
|
+
Predicate.hasProperty(value, "data") &&
|
|
38
|
+
Predicate.hasProperty(value, "init") &&
|
|
39
|
+
value.type === "DataWithResponseInit";
|
|
38
40
|
|
|
39
41
|
/** True for a plain object, which can be copied without losing behavior. */
|
|
40
42
|
const isPlainRecord = (value: unknown): value is Record<string, unknown> => {
|
|
@@ -44,15 +46,15 @@ const isPlainRecord = (value: unknown): value is Record<string, unknown> => {
|
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
|
-
* Attaches `meta` to
|
|
49
|
+
* Attaches `meta` to a loader or action result. A plain record gets a copy with the
|
|
48
50
|
* metadata key; a `data(...)` wrapper gets it on its body and keeps its init.
|
|
49
51
|
* Any other value is returned unchanged.
|
|
50
52
|
*/
|
|
51
|
-
export const
|
|
53
|
+
export const attachRouteMeta = <T>(value: T, meta: unknown): T => {
|
|
52
54
|
if (isDataWithResponseInit(value)) {
|
|
53
55
|
if (!isPlainRecord(value.data)) return value;
|
|
54
56
|
// The copy has the same shape as `value` plus the metadata key.
|
|
55
|
-
return data({ ...value.data, [
|
|
57
|
+
return data({ ...value.data, [routeMetaKey]: meta }, value.init ?? undefined) as T;
|
|
56
58
|
}
|
|
57
|
-
return isPlainRecord(value) ? ({ ...value, [
|
|
59
|
+
return isPlainRecord(value) ? ({ ...value, [routeMetaKey]: meta } as T) : value;
|
|
58
60
|
};
|
package/src/server-entry.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** `react-router-effect/server`: serve React Router from the Effect HTTP server. */
|
|
2
2
|
import "./route-data.ts";
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export { ReactRouter } from "./server.ts";
|
|
5
|
+
export * as ReactRouterAssets from "./assets.ts";
|
|
5
6
|
export type {
|
|
6
7
|
RegisterMismatch,
|
|
7
8
|
RouteEffectCheck,
|