@effectify/react-router 0.3.0 → 0.4.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.
@@ -1,3 +1,4 @@
1
1
  export * from './lib/context.js';
2
+ export * as HttpApiHandler from './lib/http-api-handler.js';
2
3
  export * from './lib/http-response.js';
3
4
  export * as Runtime from './lib/runtime.js';
package/dist/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './lib/context.js';
2
+ export * as HttpApiHandler from './lib/http-api-handler.js';
2
3
  export * from './lib/http-response.js';
3
4
  export * as Runtime from './lib/runtime.js';
@@ -0,0 +1,13 @@
1
+ /** biome-ignore-all lint/suspicious/noConsole: <debug> */
2
+ import type * as HttpApi from '@effect/platform/HttpApi';
3
+ import * as HttpApiScalar from '@effect/platform/HttpApiScalar';
4
+ import * as Layer from 'effect/Layer';
5
+ import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
6
+ export type HttpApiOptions = {
7
+ apiLive: Layer.Layer<HttpApi.Api, never, never>;
8
+ scalar?: HttpApiScalar.ScalarConfig;
9
+ };
10
+ export type RoutePath = '/' | `/${string}/`;
11
+ export declare const make: (options: HttpApiOptions & {
12
+ pathPrefix?: RoutePath;
13
+ }) => ({ request }: ActionFunctionArgs | LoaderFunctionArgs) => Promise<Response>;
@@ -0,0 +1,17 @@
1
+ import * as HttpApiBuilder from '@effect/platform/HttpApiBuilder';
2
+ import * as HttpApiScalar from '@effect/platform/HttpApiScalar';
3
+ import * as HttpServer from '@effect/platform/HttpServer';
4
+ import { Option } from 'effect';
5
+ import { pipe } from 'effect/Function';
6
+ import * as Layer from 'effect/Layer';
7
+ export const make = (options) => ({ request }) => pipe(Option.fromNullable(options.scalar), Option.map((scalar) => HttpApiScalar.layer({
8
+ path: `${options.pathPrefix || '/api/'}docs`,
9
+ scalar: {
10
+ ...scalar,
11
+ baseServerURL: new URL(request.url).origin,
12
+ },
13
+ }).pipe(Layer.provide(options.apiLive))), Option.getOrElse(() => Layer.empty), (ApiDocsLive) => {
14
+ const EnvLive = Layer.mergeAll(options.apiLive, ApiDocsLive, HttpServer.layerContext);
15
+ const { handler } = HttpApiBuilder.toWebHandler(EnvLive);
16
+ return handler(request);
17
+ });
@@ -4,21 +4,21 @@ import { type ActionFunctionArgs, type LoaderFunctionArgs } from 'react-router';
4
4
  import { ActionArgsContext, LoaderArgsContext } from '../lib/context.js';
5
5
  import { type HttpResponse } from '../lib/http-response.js';
6
6
  export declare const make: <R, E>(layer: Layer.Layer<R, E, never>) => {
7
- withLoaderEffect: <A, B>(self: Effect.Effect<HttpResponse<A>, B, R | LoaderArgsContext>) => (args: LoaderFunctionArgs) => Promise<{
7
+ withLoaderEffect: <A, B>(self: Effect.Effect<HttpResponse<A> | Response, B, R | LoaderArgsContext>) => (args: LoaderFunctionArgs) => Promise<{
8
8
  ok: true;
9
9
  data: A;
10
10
  } | {
11
11
  ok: false;
12
12
  errors: string[];
13
13
  }>;
14
- withActionEffect: <A, B_1>(self: Effect.Effect<HttpResponse<A>, B_1, R | ActionArgsContext>) => (args: ActionFunctionArgs) => Promise<import("react-router").UNSAFE_DataWithResponseInit<{
14
+ withActionEffect: <A, B_1>(self: Effect.Effect<HttpResponse<A> | Response, B_1, R | ActionArgsContext>) => (args: ActionFunctionArgs) => Promise<Response | import("react-router").UNSAFE_DataWithResponseInit<{
15
15
  ok: false;
16
- errors: B_1;
16
+ errors: string[];
17
17
  }> | {
18
18
  ok: true;
19
19
  response: A;
20
20
  } | import("react-router").UNSAFE_DataWithResponseInit<{
21
21
  ok: false;
22
22
  errors: string[];
23
- }> | Response>;
23
+ }>>;
24
24
  };
@@ -29,49 +29,75 @@ export const make = (layer) => {
29
29
  });
30
30
  }
31
31
  // Handle other types of failures (interrupts, defects, etc.)
32
+ console.error('Runtime execution failed with defect:', JSON.stringify(cause, null, 2));
32
33
  const errorData = { ok: false, errors: ['Internal server error'] };
33
34
  throw new Response(JSON.stringify(errorData), {
34
35
  status: 500,
35
36
  headers: { 'Content-Type': 'application/json' },
36
37
  });
37
38
  },
38
- onSuccess: matchHttpResponse()({
39
- HttpResponseSuccess: ({ data: response }) => {
40
- return { ok: true, data: response };
41
- },
42
- HttpResponseFailure: ({ cause }) => {
43
- // Convert HttpResponseFailure to Response for ErrorBoundary with ok: false
44
- const errorMessage = typeof cause === 'string' ? cause : String(cause);
45
- const errorData = { ok: false, errors: [errorMessage] };
46
- throw new Response(JSON.stringify(errorData), {
47
- status: 500,
48
- headers: { 'Content-Type': 'application/json' },
49
- });
50
- },
51
- HttpResponseRedirect: ({ to, init = {} }) => {
52
- redirect(to, init);
53
- return { ok: false, errors: ['Redirecting...'] };
54
- },
55
- }),
39
+ onSuccess: (result) => {
40
+ // If the result is a Response, throw it directly to preserve headers (including Set-Cookie)
41
+ // This is how React Router expects to handle Responses from loaders/actions
42
+ if (result instanceof Response) {
43
+ throw result;
44
+ }
45
+ // Otherwise, match the HttpResponse types
46
+ return matchHttpResponse()({
47
+ HttpResponseSuccess: ({ data: response }) => ({ ok: true, data: response }),
48
+ HttpResponseFailure: ({ cause }) => {
49
+ // Convert HttpResponseFailure to Response for ErrorBoundary with ok: false
50
+ const errorMessage = typeof cause === 'string' ? cause : String(cause);
51
+ const errorData = { ok: false, errors: [errorMessage] };
52
+ throw new Response(JSON.stringify(errorData), {
53
+ status: 500,
54
+ headers: { 'Content-Type': 'application/json' },
55
+ });
56
+ },
57
+ HttpResponseRedirect: ({ to, init = {} }) => {
58
+ redirect(to, init);
59
+ return { ok: false, errors: ['Redirecting...'] };
60
+ },
61
+ })(result);
62
+ },
56
63
  }));
57
64
  };
58
65
  // Don't throw the Error requests, handle them in the normal UI. No ErrorBoundary
59
66
  const withActionEffect = (self) => (args) => {
60
- const runnable = pipe(self, Effect.provide(Logger.pretty), Effect.provideService(ActionArgsContext, args), Effect.tapError((cause) => Effect.logError('Action effect failed', cause)), Effect.match({
61
- onFailure: (errors) => data({ ok: false, errors }, { status: 400 }),
62
- onSuccess: matchHttpResponse()({
63
- HttpResponseSuccess: ({ data: response }) => {
64
- return { ok: true, response };
65
- },
66
- HttpResponseFailure: ({ cause }) => {
67
- return data({ ok: false, errors: [String(cause)] }, { status: 400 });
68
- },
69
- HttpResponseRedirect: ({ to, init = {} }) => {
70
- return redirect(to, init);
71
- },
72
- }),
67
+ const runnable = pipe(self, Effect.provide(Logger.pretty), Effect.provideService(ActionArgsContext, args), Effect.tapError((cause) => {
68
+ // Don't log if it's a Response - that's expected behavior
69
+ if (!(cause instanceof Response)) {
70
+ return Effect.logError('Action effect failed', cause);
71
+ }
72
+ return Effect.void;
73
+ }));
74
+ return runtime.runPromiseExit(runnable).then(Exit.match({
75
+ onFailure: (cause) => {
76
+ if (cause._tag === 'Fail') {
77
+ const error = cause.error;
78
+ // If the error is a Response, throw it directly to preserve headers
79
+ if (error instanceof Response) {
80
+ throw error;
81
+ }
82
+ return data({ ok: false, errors: [String(error)] }, { status: 400 });
83
+ }
84
+ // Handle other types of failures
85
+ return data({ ok: false, errors: ['Internal server error'] }, { status: 400 });
86
+ },
87
+ onSuccess: (result) => {
88
+ // If the result is a Response, throw it directly to preserve headers (including Set-Cookie)
89
+ // This is how React Router expects to handle Responses from actions
90
+ if (result instanceof Response) {
91
+ throw result;
92
+ }
93
+ // Otherwise, match the HttpResponse types
94
+ return matchHttpResponse()({
95
+ HttpResponseSuccess: ({ data: response }) => ({ ok: true, response }),
96
+ HttpResponseFailure: ({ cause }) => data({ ok: false, errors: [String(cause)] }, { status: 400 }),
97
+ HttpResponseRedirect: ({ to, init = {} }) => redirect(to, init),
98
+ })(result);
99
+ },
73
100
  }));
74
- return runtime.runPromise(runnable);
75
101
  };
76
102
  return { withLoaderEffect, withActionEffect };
77
103
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectify/react-router",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Integration of React Router with Effect for React applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,10 +22,10 @@
22
22
  "!**/*.tsbuildinfo"
23
23
  ],
24
24
  "dependencies": {
25
- "@effect/platform": "0.91.1",
26
- "@effect/platform-node": "0.97.1",
27
- "effect": "3.17.14",
28
- "react-router": "7.9.2"
25
+ "@effect/platform": "0.93.3",
26
+ "@effect/platform-node": "0.101.1",
27
+ "effect": "3.19.6",
28
+ "react-router": "7.9.6"
29
29
  },
30
30
  "devDependencies": {},
31
31
  "peerDependencies": {},