@effectify/react-remix 0.2.0 → 0.4.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/README.md CHANGED
@@ -41,7 +41,7 @@ Use the Effect-based loaders and actions in your Remix routes:
41
41
  ```typescript
42
42
  // routes/home.tsx
43
43
  import type * as Route from "./+types.home";
44
- import { Ok, LoaderArgsContext } from "@effectify/react-remix";
44
+ import { httpSuccess, httpFailure, LoaderArgsContext } from "@effectify/react-remix";
45
45
  import { withLoaderEffect } from "~/lib/server-runtime";
46
46
  import * as Effect from "effect/Effect"
47
47
 
@@ -49,7 +49,12 @@ export const loader = withLoaderEffect(
49
49
  Effect.gen(function* () {
50
50
  const { request } = yield* LoaderArgsContext
51
51
  yield* Effect.log("request", request)
52
- return yield* Effect.succeed(new Ok({ data: { hello: 'world' }}))
52
+
53
+ // Improved DX: Simple syntax for success responses
54
+ return yield* httpSuccess({ hello: 'world' })
55
+
56
+ // For error responses, use:
57
+ // return yield* httpFailure("Something went wrong")
53
58
  })
54
59
  )
55
60
 
@@ -61,6 +66,23 @@ export default function Home({ loaderData }: Route.ComponentProps) {
61
66
  </div>
62
67
  )
63
68
  }
69
+
70
+ ### Error Responses
71
+
72
+ For error handling, use the `httpFailure` helper:
73
+ ```typescript
74
+ return yield* httpFailure("Something went wrong")
75
+ // or with more complex error objects
76
+ return yield* httpFailure({ code: 'VALIDATION_ERROR', message: 'Invalid input' })
77
+ ```
78
+
79
+ ### Redirects
80
+
81
+ For redirects, use the `httpRedirect` helper:
82
+ ```typescript
83
+ return yield* httpRedirect('/login')
84
+ // or with custom status/headers
85
+ return yield* httpRedirect('/dashboard', { status: 301 })
64
86
  ```
65
87
 
66
88
  ## API
@@ -88,9 +110,49 @@ Effect context providing access to Remix loader arguments including:
88
110
 
89
111
  ### Response Types
90
112
 
91
- - `Ok(data)`: Successful response with data
92
- - `Redirect(url)`: Redirect response
93
- - `Error(message)`: Error response
113
+ - `HttpResponseSuccess<T>(data)`: Successful HTTP response with data
114
+ - `HttpResponseFailure<T>(cause)`: Failed HTTP response with cause
115
+ - `HttpResponseRedirect(to, init?)`: HTTP redirect response
116
+
117
+ ### Helper Functions
118
+
119
+ - `httpSuccess<T>(data: T)`: Creates a successful Effect with HttpResponseSuccess
120
+ - `httpFailure<T>(cause: T)`: Creates a successful Effect with HttpResponseFailure
121
+ - `httpRedirect(to: string, init?: ResponseInit)`: Creates a successful Effect with HttpResponseRedirect
122
+
123
+ ### Error Handling & Logging
124
+
125
+ The library provides comprehensive error handling with full ErrorBoundary support:
126
+
127
+ - **Automatic Error Logging**: Errors are automatically logged using `Effect.logError`
128
+ - **ErrorBoundary Compatible**: Loader errors are properly thrown as `Response` objects for Remix ErrorBoundary
129
+ - **Configurable Logging**: Users can configure their own logging system through Effect layers
130
+ - **Non-blocking**: Logging doesn't block the main thread
131
+ - **Structured Logging**: Errors are logged with context and structured data
132
+ - **Error Preservation**: Original error context is preserved for better debugging
133
+
134
+ ```typescript
135
+ // The library automatically logs errors like this:
136
+ Effect.tapError((cause) => Effect.logError('Loader effect failed', cause))
137
+
138
+ // Loader errors are automatically converted to Response objects for ErrorBoundary:
139
+ // - Effect errors → Response with { ok: false, errors: [...] } and status 500
140
+ // - HttpResponseFailure → Response with { ok: false, errors: [...] } and status 500
141
+ // - Original Response/Error objects are preserved
142
+
143
+ // Users can configure custom logging through Effect layers
144
+ const customLogger = Logger.make(({ message, cause }) => {
145
+ // Custom logging implementation
146
+ console.log(`[${new Date().toISOString()}] ${message}`, cause)
147
+ })
148
+
149
+ const runtime = make(pipe(
150
+ // Your app layers
151
+ MyAppLayer,
152
+ // Custom logger layer
153
+ Logger.replace(Logger.defaultLogger, customLogger)
154
+ ))
155
+ ```
94
156
 
95
157
  ## Requirements
96
158
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectify/react-remix",
3
- "version": "0.2.0",
3
+ "version": "0.4.1",
4
4
  "description": "Integration of Remix with Effect for React applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,11 +22,11 @@
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
- "@remix-run/react": "2.17.1",
29
- "@remix-run/node": "2.17.1"
25
+ "@effect/platform": "0.94.0",
26
+ "@effect/platform-node": "0.104.0",
27
+ "effect": "3.19.13",
28
+ "@remix-run/react": "2.17.2",
29
+ "@remix-run/node": "2.17.2"
30
30
  },
31
31
  "devDependencies": {},
32
32
  "peerDependencies": {},
@@ -1,3 +0,0 @@
1
- export * from './lib/context.js';
2
- export * from './lib/http-response.js';
3
- export * as Runtime from './lib/runtime.js';
package/dist/src/index.js DELETED
@@ -1,3 +0,0 @@
1
- export * from './lib/context.js';
2
- export * from './lib/http-response.js';
3
- export * as Runtime from './lib/runtime.js';
@@ -1,9 +0,0 @@
1
- import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node';
2
- import * as Context from 'effect/Context';
3
- declare const ActionArgsContext_base: Context.TagClass<ActionArgsContext, "ActionArgsContext", ActionFunctionArgs>;
4
- export declare class ActionArgsContext extends ActionArgsContext_base {
5
- }
6
- declare const LoaderArgsContext_base: Context.TagClass<LoaderArgsContext, "LoaderArgsContext", LoaderFunctionArgs>;
7
- export declare class LoaderArgsContext extends LoaderArgsContext_base {
8
- }
9
- export {};
@@ -1,5 +0,0 @@
1
- import * as Context from 'effect/Context';
2
- export class ActionArgsContext extends Context.Tag('ActionArgsContext')() {
3
- }
4
- export class LoaderArgsContext extends Context.Tag('LoaderArgsContext')() {
5
- }
@@ -1,21 +0,0 @@
1
- declare const Ok_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => Readonly<A> & {
2
- readonly _tag: "Ok";
3
- };
4
- export declare class Ok<T> extends Ok_base<{
5
- readonly data: T;
6
- }> {
7
- }
8
- declare const Redirect_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => Readonly<A> & {
9
- readonly _tag: "Redirect";
10
- };
11
- export declare class Redirect extends Redirect_base<{
12
- readonly to: string;
13
- readonly init?: number | ResponseInit | undefined;
14
- }> {
15
- }
16
- export type HttpResponse<T> = Redirect | Ok<T>;
17
- export declare const matchHttpResponse: <T>() => <P extends {
18
- readonly Ok: (_: Ok<T>) => any;
19
- readonly Redirect: (_: Redirect) => any;
20
- } & { readonly [Tag in Exclude<keyof P, "Ok" | "Redirect">]: never; }>(fields: P) => (input: HttpResponse<T>) => import("effect/Unify").Unify<ReturnType<P[keyof P]>>;
21
- export {};
@@ -1,7 +0,0 @@
1
- import * as Data from 'effect/Data';
2
- import * as Match from 'effect/Match';
3
- export class Ok extends Data.TaggedClass('Ok') {
4
- }
5
- export class Redirect extends Data.TaggedClass('Redirect') {
6
- }
7
- export const matchHttpResponse = () => Match.typeTags();
@@ -1,21 +0,0 @@
1
- import { type ActionFunctionArgs, type LoaderFunctionArgs } from '@remix-run/node';
2
- import * as Effect from 'effect/Effect';
3
- import type * as Layer from 'effect/Layer';
4
- import { ActionArgsContext, LoaderArgsContext } from './context.js';
5
- import { type HttpResponse } from './http-response.js';
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<{
8
- ok: true;
9
- data: A;
10
- } | {
11
- ok: false;
12
- errors: string[];
13
- }>;
14
- withActionEffect: <A, B_1>(self: Effect.Effect<HttpResponse<A>, B_1, R | ActionArgsContext>) => (args: ActionFunctionArgs) => Promise<import("@remix-run/node").TypedResponse<{
15
- ok: false;
16
- errors: B_1;
17
- }> | {
18
- ok: true;
19
- response: A;
20
- } | import("@remix-run/node").TypedResponse<never>>;
21
- };
@@ -1,49 +0,0 @@
1
- import { json, redirect } from '@remix-run/node';
2
- import * as Effect from 'effect/Effect';
3
- import * as Exit from 'effect/Exit';
4
- import { pipe } from 'effect/Function';
5
- import * as Logger from 'effect/Logger';
6
- import * as ManagedRuntime from 'effect/ManagedRuntime';
7
- import { ActionArgsContext, LoaderArgsContext } from './context.js';
8
- import { matchHttpResponse } from './http-response.js';
9
- export const make = (layer) => {
10
- const runtime = ManagedRuntime.make(layer);
11
- const withLoaderEffect = (self) => (args) => {
12
- const runnable = pipe(self, Effect.provide(Logger.pretty), Effect.provideService(LoaderArgsContext, args));
13
- return runtime.runPromiseExit(runnable).then(Exit.match({
14
- onFailure: (cause) => {
15
- console.error(cause);
16
- if (cause._tag === 'Fail') {
17
- throw pipe(cause.error);
18
- }
19
- // biome-ignore lint/style/useThrowOnlyError: <library uses non-Error throws>
20
- throw { ok: false, errors: ['Something went wrong'] };
21
- },
22
- onSuccess: matchHttpResponse()({
23
- Ok: ({ data: response }) => {
24
- return { ok: true, data: response };
25
- },
26
- Redirect: ({ to, init = {} }) => {
27
- redirect(to, init);
28
- return { ok: false, errors: ['Redirecting...'] };
29
- },
30
- }),
31
- }));
32
- };
33
- // Don't throw the Error requests, handle them in the normal UI. No ErrorBoundary
34
- const withActionEffect = (self) => (args) => {
35
- const runnable = pipe(self, Effect.provide(Logger.pretty), Effect.provideService(ActionArgsContext, args), Effect.match({
36
- onFailure: (errors) => json({ ok: false, errors }, { status: 400 }),
37
- onSuccess: matchHttpResponse()({
38
- Ok: ({ data: response }) => {
39
- return { ok: true, response };
40
- },
41
- Redirect: ({ to, init = {} }) => {
42
- return redirect(to, init);
43
- },
44
- }),
45
- }));
46
- return runtime.runPromise(runnable);
47
- };
48
- return { withLoaderEffect, withActionEffect };
49
- };