@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 +67 -5
- package/package.json +6 -6
- package/dist/src/index.d.ts +0 -3
- package/dist/src/index.js +0 -3
- package/dist/src/lib/context.d.ts +0 -9
- package/dist/src/lib/context.js +0 -5
- package/dist/src/lib/http-response.d.ts +0 -21
- package/dist/src/lib/http-response.js +0 -7
- package/dist/src/lib/runtime.d.ts +0 -21
- package/dist/src/lib/runtime.js +0 -49
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 {
|
|
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
|
-
|
|
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
|
-
- `
|
|
92
|
-
- `
|
|
93
|
-
- `
|
|
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.
|
|
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.
|
|
26
|
-
"@effect/platform-node": "0.
|
|
27
|
-
"effect": "3.
|
|
28
|
-
"@remix-run/react": "2.17.
|
|
29
|
-
"@remix-run/node": "2.17.
|
|
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": {},
|
package/dist/src/index.d.ts
DELETED
package/dist/src/index.js
DELETED
|
@@ -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 {};
|
package/dist/src/lib/context.js
DELETED
|
@@ -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,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
|
-
};
|
package/dist/src/lib/runtime.js
DELETED
|
@@ -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
|
-
};
|