react-router-effect 1.0.0 → 1.0.1

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/dist/index.d.mts CHANGED
@@ -101,8 +101,10 @@ type MiddlewareNextOf<Middleware extends AnyMiddlewareFunction> = Parameters<Mid
101
101
  /**
102
102
  * The `Result` a `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) resolves
103
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.
104
106
  */
105
- type MiddlewareResultOf<Middleware extends AnyMiddlewareFunction> = MiddlewareNextOf<Middleware> extends (() => Promise<infer Result>) ? Result : never;
107
+ type MiddlewareResultOf<Middleware extends AnyMiddlewareFunction> = MiddlewareNextOf<Middleware> extends ((...args: never[]) => infer Result) ? Awaited<Result> : never;
106
108
  /**
107
109
  * The inferred map of handler RETURN types, keyed by domain-error tag. Making the
108
110
  * factory generic over *this* (rather than the whole handler map) is what lets
@@ -183,10 +185,10 @@ type NameError<U> = U extends {
183
185
  */
184
186
  type IsAny<T> = 0 extends 1 & T ? true : false;
185
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 : {
186
- "react-router-effect": "This loader/action requires a service the factory does not provide. Add it to your runtime or requestContext, or handle it in the effect.";
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.";
187
189
  missingRequirements: Exclude<R, Provided>;
188
190
  } : {
189
- "react-router-effect": "This loader/action 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.";
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.";
190
192
  unhandledErrors: NameError<Unhandled<E, DomainError, AlsoHandled>>;
191
193
  };
192
194
  /**
@@ -329,7 +331,7 @@ declare function makeEffectRouteFactory<DomainError extends Tagged = never>(): <
329
331
  }) => {
330
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>>;
331
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>>;
332
- makeMiddleware: <Middleware extends MiddlewareFunction<any> = MiddlewareFunction, A = MiddlewareResultOf<Middleware>, E = never, R = never>(fn: (args: MiddlewareArgsOf<Middleware>, next: () => Effect.Effect<MiddlewareResultOf<Middleware>, FailureResponse>) => Effect.Effect<A, E, R> & Diagnose<E, R, DomainError, RServices | ReqServices, FailureResponse>) => (args: MiddlewareArgsOf<Middleware>, next: MiddlewareNextOf<Middleware>) => 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>;
333
335
  Respond: {
334
336
  early: <D>(value: D, init?: number | ResponseInit) => ReturnableDataError<D>;
335
337
  throw: <D>(value: D, init?: number | ResponseInit) => ThrowableDataError<D>;
package/dist/index.mjs CHANGED
@@ -170,6 +170,23 @@ function makeEffectRouteFactory() {
170
170
  const run = fn;
171
171
  return (args) => runProgram(run(args), args.context);
172
172
  }
173
+ /**
174
+ * Wraps a middleware function written as an effect. Pin it to the route's generated
175
+ * `Route.MiddlewareFunction` (a type argument, since middleware has no separate
176
+ * args-only type) to type `args`, `next` and the result from that route; it defaults
177
+ * to React Router's own `MiddlewareFunction`.
178
+ *
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 — plus the
181
+ * `FailureResponse` `next()` fails with. Anything else must be handled in the effect,
182
+ * as must any service neither the `runtime` nor the `requestContext` provides.
183
+ *
184
+ * The wrapper is typed as the route's own middleware — `(args, next) =>
185
+ * Promise<Result | undefined>` — so it drops straight into a
186
+ * `Route.MiddlewareFunction[]`. A handler that recovers (`Respond.early`) resolves
187
+ * with `data(...)`, which react-router's server middleware pipeline turns into the
188
+ * short-circuit `Response`.
189
+ */
173
190
  function makeMiddleware(fn) {
174
191
  const run = fn;
175
192
  return (args, next) => {
@@ -177,7 +194,8 @@ function makeEffectRouteFactory() {
177
194
  try: next,
178
195
  catch: (e) => e
179
196
  });
180
- return runProgram(run(args, effectNext), args.context);
197
+ const { context } = args;
198
+ return runProgram(run(args, effectNext), context);
181
199
  };
182
200
  }
183
201
  return {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schemastore.org/package.json",
3
3
  "name": "react-router-effect",
4
- "version": "1.0.0",
4
+ "version": "1.0.1",
5
5
  "description": "Wrap React Router framework-mode route modules with Effect, with typed recover/throw/redirect error handling.",
6
6
  "keywords": [
7
7
  "action",
@@ -44,14 +44,14 @@
44
44
  "@vitejs/plugin-react": "^6.0.4",
45
45
  "@vitest/browser": "4.1.10",
46
46
  "@vitest/browser-playwright": "4.1.10",
47
- "effect": "4.0.0-beta.101",
47
+ "effect": "4.0.0-beta.102",
48
48
  "playwright": "1.60.0",
49
49
  "react": "^19.2.8",
50
50
  "react-dom": "^19.2.8",
51
51
  "react-router": "^8.3.0",
52
52
  "typescript": "^7.0.2",
53
53
  "vite-plus": "latest",
54
- "vitest": "4.1.9",
54
+ "vitest": "4.1.10",
55
55
  "vitest-browser-react": "^2.2.0"
56
56
  },
57
57
  "peerDependencies": {
package/src/factory.ts CHANGED
@@ -81,9 +81,11 @@ type MiddlewareNextOf<Middleware extends AnyMiddlewareFunction> = Parameters<Mid
81
81
  /**
82
82
  * The `Result` a `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) resolves
83
83
  * with — i.e. what its `next()` resolves to, and what the middleware itself may return.
84
+ * Read through `Awaited` because react-router declares `next` as an *interface* with a
85
+ * call signature (`MiddlewareNextFunction`), and a route's own may resolve synchronously.
84
86
  */
85
87
  type MiddlewareResultOf<Middleware extends AnyMiddlewareFunction> =
86
- MiddlewareNextOf<Middleware> extends () => Promise<infer Result> ? Result : never;
88
+ MiddlewareNextOf<Middleware> extends (...args: never[]) => infer Result ? Awaited<Result> : never;
87
89
 
88
90
  /**
89
91
  * The inferred map of handler RETURN types, keyed by domain-error tag. Making the
@@ -195,11 +197,11 @@ type Diagnose<E, R, DomainError, Provided, AlsoHandled = never> =
195
197
  ? [Exclude<R, Provided>] extends [never]
196
198
  ? unknown
197
199
  : {
198
- "react-router-effect": "This loader/action requires a service the factory does not provide. Add it to your runtime or requestContext, or handle it in the effect.";
200
+ "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.";
199
201
  missingRequirements: Exclude<R, Provided>;
200
202
  }
201
203
  : {
202
- "react-router-effect": "This loader/action 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.";
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.";
203
205
  unhandledErrors: NameError<Unhandled<E, DomainError, AlsoHandled>>;
204
206
  };
205
207
 
@@ -489,35 +491,64 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
489
491
  >;
490
492
  }
491
493
 
494
+ /**
495
+ * Wraps a middleware function written as an effect. Pin it to the route's generated
496
+ * `Route.MiddlewareFunction` (a type argument, since middleware has no separate
497
+ * args-only type) to type `args`, `next` and the result from that route; it defaults
498
+ * to React Router's own `MiddlewareFunction`.
499
+ *
500
+ * The errors the library deals with for a loader/action it deals with here too —
501
+ * declared domain errors, library route errors and respondables — plus the
502
+ * `FailureResponse` `next()` fails with. Anything else must be handled in the effect,
503
+ * as must any service neither the `runtime` nor the `requestContext` provides.
504
+ *
505
+ * The wrapper is typed as the route's own middleware — `(args, next) =>
506
+ * Promise<Result | undefined>` — so it drops straight into a
507
+ * `Route.MiddlewareFunction[]`. A handler that recovers (`Respond.early`) resolves
508
+ * with `data(...)`, which react-router's server middleware pipeline turns into the
509
+ * short-circuit `Response`.
510
+ */
492
511
  function makeMiddleware<
493
- Middleware extends MiddlewareFunction<any> = MiddlewareFunction,
494
- A = MiddlewareResultOf<Middleware>,
495
- E = never,
496
- R = never,
512
+ Middleware extends AnyMiddlewareFunction = MiddlewareFunction,
513
+ // A pinned call passes an explicit type argument, which makes the whole call
514
+ // explicit — TypeScript has no partial inference — so these defaults stand in
515
+ // for what would otherwise be inferred: exactly the errors the library handles
516
+ // and the services the factory provides.
517
+ E = LibraryHandled<DomainError, FailureResponse>,
518
+ R = RServices | ReqServices,
497
519
  >(
498
520
  // Same shape as `makeLoaderOrAction`'s `fn`, but middleware also receives
499
521
  // `next` — wrapped as an effect (rather than react-router's raw
500
522
  // `() => Promise<Result>`) so downstream failures (e.g. a nested loader's
501
523
  // redirect) surface as a typed `FailureResponse`, consistent with the rest
502
- // of the library's error model.
524
+ // of the library's error model. The success type is fixed to the route's own
525
+ // `Result` (plus `void`), which is what react-router accepts back.
503
526
  fn: (
504
527
  args: MiddlewareArgsOf<Middleware>,
505
528
  next: () => Effect.Effect<MiddlewareResultOf<Middleware>, FailureResponse>,
506
- ) => Effect.Effect<A, E, R> &
529
+ ) => Effect.Effect<MiddlewareResultOf<Middleware> | void, E, R> &
507
530
  Diagnose<E, R, DomainError, RServices | ReqServices, FailureResponse>,
508
531
  ): (
509
532
  args: MiddlewareArgsOf<Middleware>,
510
533
  next: MiddlewareNextOf<Middleware>,
511
- ) => Promise<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>> {
534
+ ) => Promise<MiddlewareResultOf<Middleware> | undefined> {
535
+ // Inside the body the route's own arg/result types are opaque (they're
536
+ // `Parameters<Middleware>[...]` of an unresolved type parameter), so dispatch
537
+ // through the loose shape — as `makeLoaderOrAction` does with its diagnostic.
538
+ // Sound at runtime: the values produced are exactly the declared types.
512
539
  const run = fn as unknown as (
513
540
  args: MiddlewareArgsOf<Middleware>,
514
- next: () => Effect.Effect<MiddlewareResultOf<Middleware>, FailureResponse>,
541
+ next: () => Effect.Effect<unknown, FailureResponse>,
515
542
  ) => Effect.Effect<unknown, E, RServices | ReqServices>;
516
543
  return (args, next) => {
517
544
  const effectNext = () =>
518
- Effect.tryPromise({ try: next, catch: (e) => e as FailureResponse });
519
- return runProgram(run(args, effectNext), args.context) as Promise<
520
- A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>
545
+ Effect.tryPromise({
546
+ try: next as unknown as () => Promise<unknown>,
547
+ catch: (e) => e as FailureResponse,
548
+ });
549
+ const { context } = args as unknown as { context: Readonly<RouterContextProvider> };
550
+ return runProgram(run(args, effectNext), context) as Promise<
551
+ MiddlewareResultOf<Middleware> | undefined
521
552
  >;
522
553
  };
523
554
  }