@effectify/react-router 0.4.9 → 0.5.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 +24 -18
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.js +4 -4
- package/dist/src/lib/context.d.ts +2 -2
- package/dist/src/lib/context.js +3 -3
- package/dist/src/lib/http-api-handler.d.ts +5 -6
- package/dist/src/lib/http-api-handler.js +7 -7
- package/dist/src/lib/http-response.d.ts +1 -1
- package/dist/src/lib/http-response.js +6 -6
- package/dist/src/lib/runtime.d.ts +5 -5
- package/dist/src/lib/runtime.js +20 -20
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Create a server runtime with your Effect layers:
|
|
|
26
26
|
|
|
27
27
|
```typescript
|
|
28
28
|
// lib/server-runtime.ts
|
|
29
|
-
import { make } from "@effectify/react-router"
|
|
29
|
+
import { make } from "@effectify/react-router"
|
|
30
30
|
import * as Layer from "effect/Layer"
|
|
31
31
|
|
|
32
32
|
const layers = Layer.empty
|
|
@@ -40,22 +40,22 @@ Use the Effect-based loaders and actions in your React Router routes:
|
|
|
40
40
|
|
|
41
41
|
```typescript
|
|
42
42
|
// routes/home.tsx
|
|
43
|
-
import type * as Route from "./+types.home"
|
|
44
|
-
import {
|
|
45
|
-
import { withLoaderEffect } from "~/lib/server-runtime"
|
|
43
|
+
import type * as Route from "./+types.home"
|
|
44
|
+
import { httpFailure, httpSuccess, LoaderArgsContext } from "@effectify/react-router"
|
|
45
|
+
import { withLoaderEffect } from "~/lib/server-runtime"
|
|
46
46
|
import * as Effect from "effect/Effect"
|
|
47
47
|
|
|
48
48
|
export const loader = withLoaderEffect(
|
|
49
|
-
Effect.gen(function*
|
|
49
|
+
Effect.gen(function*() {
|
|
50
50
|
const { request } = yield* LoaderArgsContext
|
|
51
51
|
yield* Effect.log("request", request)
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
// Improved DX: Simple syntax for success responses
|
|
54
|
-
return yield* httpSuccess({ hello:
|
|
55
|
-
|
|
54
|
+
return yield* httpSuccess({ hello: "world" })
|
|
55
|
+
|
|
56
56
|
// For error responses, use:
|
|
57
57
|
// return yield* httpFailure("Something went wrong")
|
|
58
|
-
})
|
|
58
|
+
}),
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
export default function Home({ loaderData }: Route.ComponentProps) {
|
|
@@ -75,31 +75,35 @@ The library provides helper functions for better DX when returning HTTP response
|
|
|
75
75
|
### Success Responses
|
|
76
76
|
|
|
77
77
|
Instead of the verbose:
|
|
78
|
+
|
|
78
79
|
```typescript
|
|
79
|
-
return yield* Effect.succeed(new HttpResponseSuccess({ data: { hello:
|
|
80
|
+
return yield * Effect.succeed(new HttpResponseSuccess({ data: { hello: "world" } }))
|
|
80
81
|
```
|
|
81
82
|
|
|
82
83
|
Use the simplified syntax:
|
|
84
|
+
|
|
83
85
|
```typescript
|
|
84
|
-
return yield* httpSuccess({ hello:
|
|
86
|
+
return yield * httpSuccess({ hello: "world" })
|
|
85
87
|
```
|
|
86
88
|
|
|
87
89
|
### Error Responses
|
|
88
90
|
|
|
89
91
|
For error handling, use the `httpFailure` helper:
|
|
92
|
+
|
|
90
93
|
```typescript
|
|
91
|
-
return yield* httpFailure("Something went wrong")
|
|
94
|
+
return yield * httpFailure("Something went wrong")
|
|
92
95
|
// or with more complex error objects
|
|
93
|
-
return yield* httpFailure({ code:
|
|
96
|
+
return yield * httpFailure({ code: "VALIDATION_ERROR", message: "Invalid input" })
|
|
94
97
|
```
|
|
95
98
|
|
|
96
99
|
### Redirects
|
|
97
100
|
|
|
98
101
|
For redirects, use the `httpRedirect` helper:
|
|
102
|
+
|
|
99
103
|
```typescript
|
|
100
|
-
return yield* httpRedirect(
|
|
104
|
+
return yield * httpRedirect("/login")
|
|
101
105
|
// or with custom status/headers
|
|
102
|
-
return yield* httpRedirect(
|
|
106
|
+
return yield * httpRedirect("/dashboard", { status: 301 })
|
|
103
107
|
```
|
|
104
108
|
|
|
105
109
|
## API
|
|
@@ -115,12 +119,14 @@ Creates Effect-based runtime helpers for React Router.
|
|
|
115
119
|
#### Returns
|
|
116
120
|
|
|
117
121
|
An object containing:
|
|
122
|
+
|
|
118
123
|
- `withLoaderEffect`: Wrapper for React Router loaders using Effect
|
|
119
124
|
- `withActionEffect`: Wrapper for React Router actions using Effect
|
|
120
125
|
|
|
121
126
|
### `LoaderArgsContext`
|
|
122
127
|
|
|
123
128
|
Effect context providing access to React Router loader arguments including:
|
|
129
|
+
|
|
124
130
|
- `request`: The incoming Request object
|
|
125
131
|
- `params`: Route parameters
|
|
126
132
|
- `context`: Additional context data
|
|
@@ -150,7 +156,7 @@ The library provides comprehensive error handling with full ErrorBoundary suppor
|
|
|
150
156
|
|
|
151
157
|
```typescript
|
|
152
158
|
// The library automatically logs errors like this:
|
|
153
|
-
Effect.tapError((cause) => Effect.logError(
|
|
159
|
+
Effect.tapError((cause) => Effect.logError("Loader effect failed", cause))
|
|
154
160
|
|
|
155
161
|
// Loader errors are automatically converted to Response objects for ErrorBoundary:
|
|
156
162
|
// - Effect errors → Response with { ok: false, errors: [...] } and status 500
|
|
@@ -167,7 +173,7 @@ const runtime = make(pipe(
|
|
|
167
173
|
// Your app layers
|
|
168
174
|
MyAppLayer,
|
|
169
175
|
// Custom logger layer
|
|
170
|
-
Logger.replace(Logger.defaultLogger, customLogger)
|
|
176
|
+
Logger.replace(Logger.defaultLogger, customLogger),
|
|
171
177
|
))
|
|
172
178
|
```
|
|
173
179
|
|
|
@@ -178,4 +184,4 @@ const runtime = make(pipe(
|
|
|
178
184
|
|
|
179
185
|
## License
|
|
180
186
|
|
|
181
|
-
MIT
|
|
187
|
+
MIT
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * as HttpApiHandler from
|
|
3
|
-
export * from
|
|
4
|
-
export * as Runtime from
|
|
1
|
+
export * from "./lib/context.js";
|
|
2
|
+
export * as HttpApiHandler from "./lib/http-api-handler.js";
|
|
3
|
+
export * from "./lib/http-response.js";
|
|
4
|
+
export * as Runtime from "./lib/runtime.js";
|
package/dist/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * as HttpApiHandler from
|
|
3
|
-
export * from
|
|
4
|
-
export * as Runtime from
|
|
1
|
+
export * from "./lib/context.js";
|
|
2
|
+
export * as HttpApiHandler from "./lib/http-api-handler.js";
|
|
3
|
+
export * from "./lib/http-response.js";
|
|
4
|
+
export * as Runtime from "./lib/runtime.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as Context from
|
|
2
|
-
import type { ActionFunctionArgs, LoaderFunctionArgs } from
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
|
|
3
3
|
declare const ActionArgsContext_base: Context.TagClass<ActionArgsContext, "ActionArgsContext", ActionFunctionArgs<any>>;
|
|
4
4
|
export declare class ActionArgsContext extends ActionArgsContext_base {
|
|
5
5
|
}
|
package/dist/src/lib/context.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as Context from
|
|
2
|
-
export class ActionArgsContext extends Context.Tag(
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
export class ActionArgsContext extends Context.Tag("ActionArgsContext")() {
|
|
3
3
|
}
|
|
4
|
-
export class LoaderArgsContext extends Context.Tag(
|
|
4
|
+
export class LoaderArgsContext extends Context.Tag("LoaderArgsContext")() {
|
|
5
5
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import * as
|
|
4
|
-
import
|
|
5
|
-
import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
|
|
1
|
+
import type * as HttpApi from "@effect/platform/HttpApi";
|
|
2
|
+
import * as HttpApiScalar from "@effect/platform/HttpApiScalar";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
|
|
6
5
|
export type HttpApiOptions = {
|
|
7
6
|
apiLive: Layer.Layer<HttpApi.Api, never, never>;
|
|
8
7
|
scalar?: HttpApiScalar.ScalarConfig;
|
|
9
8
|
};
|
|
10
|
-
export type RoutePath =
|
|
9
|
+
export type RoutePath = "/" | `/${string}/`;
|
|
11
10
|
export declare const make: (options: HttpApiOptions & {
|
|
12
11
|
pathPrefix?: RoutePath;
|
|
13
12
|
}) => ({ request }: ActionFunctionArgs | LoaderFunctionArgs) => Promise<Response>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import * as HttpApiBuilder from
|
|
2
|
-
import * as HttpApiScalar from
|
|
3
|
-
import * as HttpServer from
|
|
4
|
-
import { Option } from
|
|
5
|
-
import { pipe } from
|
|
6
|
-
import * as Layer from
|
|
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
7
|
export const make = (options) => ({ request }) => pipe(Option.fromNullable(options.scalar), Option.map((scalar) => HttpApiScalar.layer({
|
|
8
|
-
path: `${options.pathPrefix ||
|
|
8
|
+
path: `${options.pathPrefix || "/api/"}docs`,
|
|
9
9
|
scalar: {
|
|
10
10
|
...scalar,
|
|
11
11
|
baseServerURL: new URL(request.url).origin,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as Effect from
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
2
|
declare const HttpResponseSuccess_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> & {
|
|
3
3
|
readonly _tag: "HttpResponseSuccess";
|
|
4
4
|
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import * as Data from
|
|
2
|
-
import * as Effect from
|
|
3
|
-
import * as Match from
|
|
4
|
-
export class HttpResponseSuccess extends Data.TaggedClass(
|
|
1
|
+
import * as Data from "effect/Data";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Match from "effect/Match";
|
|
4
|
+
export class HttpResponseSuccess extends Data.TaggedClass("HttpResponseSuccess") {
|
|
5
5
|
}
|
|
6
|
-
export class HttpResponseFailure extends Data.TaggedClass(
|
|
6
|
+
export class HttpResponseFailure extends Data.TaggedClass("HttpResponseFailure") {
|
|
7
7
|
}
|
|
8
|
-
export class HttpResponseRedirect extends Data.TaggedClass(
|
|
8
|
+
export class HttpResponseRedirect extends Data.TaggedClass("HttpResponseRedirect") {
|
|
9
9
|
}
|
|
10
10
|
export const matchHttpResponse = () => Match.typeTags();
|
|
11
11
|
// Helper functions for better DX
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import * as Effect from
|
|
2
|
-
import type * as Layer from
|
|
3
|
-
import { type ActionFunctionArgs, type LoaderFunctionArgs } from
|
|
4
|
-
import { ActionArgsContext, LoaderArgsContext } from
|
|
5
|
-
import { type HttpResponse } from
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import type * as Layer from "effect/Layer";
|
|
3
|
+
import { type ActionFunctionArgs, type LoaderFunctionArgs } from "react-router";
|
|
4
|
+
import { ActionArgsContext, LoaderArgsContext } from "../lib/context.js";
|
|
5
|
+
import { type HttpResponse } from "../lib/http-response.js";
|
|
6
6
|
export declare const make: <R, E>(layer: Layer.Layer<R, E, never>) => {
|
|
7
7
|
withLoaderEffect: <A, B, R0 extends R | LoaderArgsContext>(self: Effect.Effect<HttpResponse<A> | Response, B, R0>) => (args: LoaderFunctionArgs) => Promise<{
|
|
8
8
|
ok: true;
|
package/dist/src/lib/runtime.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import * as Effect from
|
|
2
|
-
import * as Exit from
|
|
3
|
-
import { pipe } from
|
|
4
|
-
import * as Logger from
|
|
5
|
-
import * as ManagedRuntime from
|
|
6
|
-
import { data, redirect } from
|
|
7
|
-
import { ActionArgsContext, LoaderArgsContext } from
|
|
8
|
-
import { matchHttpResponse } from
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Exit from "effect/Exit";
|
|
3
|
+
import { pipe } from "effect/Function";
|
|
4
|
+
import * as Logger from "effect/Logger";
|
|
5
|
+
import * as ManagedRuntime from "effect/ManagedRuntime";
|
|
6
|
+
import { data, redirect } from "react-router";
|
|
7
|
+
import { ActionArgsContext, LoaderArgsContext } from "../lib/context.js";
|
|
8
|
+
import { matchHttpResponse } from "../lib/http-response.js";
|
|
9
9
|
export const make = (layer) => {
|
|
10
10
|
const runtime = ManagedRuntime.make(layer);
|
|
11
11
|
const withLoaderEffect = (self) => (args) => {
|
|
12
|
-
const runnable = pipe(self, Effect.provide(Logger.pretty), Effect.provideService(LoaderArgsContext, args), Effect.tapError((cause) => Effect.logError(
|
|
12
|
+
const runnable = pipe(self, Effect.provide(Logger.pretty), Effect.provideService(LoaderArgsContext, args), Effect.tapError((cause) => Effect.logError("Loader effect failed", cause)));
|
|
13
13
|
return runtime.runPromiseExit(runnable).then(Exit.match({
|
|
14
14
|
onFailure: (cause) => {
|
|
15
|
-
if (cause._tag ===
|
|
15
|
+
if (cause._tag === "Fail") {
|
|
16
16
|
// Preserve the original error for ErrorBoundary
|
|
17
17
|
const error = cause.error;
|
|
18
18
|
if (error instanceof Response) {
|
|
@@ -25,15 +25,15 @@ export const make = (layer) => {
|
|
|
25
25
|
const errorData = { ok: false, errors: [String(error)] };
|
|
26
26
|
throw new Response(JSON.stringify(errorData), {
|
|
27
27
|
status: 500,
|
|
28
|
-
headers: {
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
// Handle other types of failures (interrupts, defects, etc.)
|
|
32
|
-
console.error(
|
|
33
|
-
const errorData = { ok: false, errors: [
|
|
32
|
+
console.error("Runtime execution failed with defect:", JSON.stringify(cause, null, 2));
|
|
33
|
+
const errorData = { ok: false, errors: ["Internal server error"] };
|
|
34
34
|
throw new Response(JSON.stringify(errorData), {
|
|
35
35
|
status: 500,
|
|
36
|
-
headers: {
|
|
36
|
+
headers: { "Content-Type": "application/json" },
|
|
37
37
|
});
|
|
38
38
|
},
|
|
39
39
|
onSuccess: (result) => {
|
|
@@ -47,16 +47,16 @@ export const make = (layer) => {
|
|
|
47
47
|
HttpResponseSuccess: ({ data: response }) => ({ ok: true, data: response }),
|
|
48
48
|
HttpResponseFailure: ({ cause }) => {
|
|
49
49
|
// Convert HttpResponseFailure to Response for ErrorBoundary with ok: false
|
|
50
|
-
const errorMessage = typeof cause ===
|
|
50
|
+
const errorMessage = typeof cause === "string" ? cause : String(cause);
|
|
51
51
|
const errorData = { ok: false, errors: [errorMessage] };
|
|
52
52
|
throw new Response(JSON.stringify(errorData), {
|
|
53
53
|
status: 500,
|
|
54
|
-
headers: {
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
55
|
});
|
|
56
56
|
},
|
|
57
57
|
HttpResponseRedirect: ({ to, init = {} }) => {
|
|
58
58
|
redirect(to, init);
|
|
59
|
-
return { ok: false, errors: [
|
|
59
|
+
return { ok: false, errors: ["Redirecting..."] };
|
|
60
60
|
},
|
|
61
61
|
})(result);
|
|
62
62
|
},
|
|
@@ -67,13 +67,13 @@ export const make = (layer) => {
|
|
|
67
67
|
const runnable = pipe(self, Effect.provide(Logger.pretty), Effect.provideService(ActionArgsContext, args), Effect.tapError((cause) => {
|
|
68
68
|
// Don't log if it's a Response - that's expected behavior
|
|
69
69
|
if (!(cause instanceof Response)) {
|
|
70
|
-
return Effect.logError(
|
|
70
|
+
return Effect.logError("Action effect failed", cause);
|
|
71
71
|
}
|
|
72
72
|
return Effect.void;
|
|
73
73
|
}));
|
|
74
74
|
return runtime.runPromiseExit(runnable).then(Exit.match({
|
|
75
75
|
onFailure: (cause) => {
|
|
76
|
-
if (cause._tag ===
|
|
76
|
+
if (cause._tag === "Fail") {
|
|
77
77
|
const error = cause.error;
|
|
78
78
|
// If the error is a Response, throw it directly to preserve headers
|
|
79
79
|
if (error instanceof Response) {
|
|
@@ -82,7 +82,7 @@ export const make = (layer) => {
|
|
|
82
82
|
return data({ ok: false, errors: [String(error)] }, { status: 400 });
|
|
83
83
|
}
|
|
84
84
|
// Handle other types of failures
|
|
85
|
-
return data({ ok: false, errors: [
|
|
85
|
+
return data({ ok: false, errors: ["Internal server error"] }, { status: 400 });
|
|
86
86
|
},
|
|
87
87
|
onSuccess: (result) => {
|
|
88
88
|
// If the result is a Response, throw it directly to preserve headers (including Set-Cookie)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effectify/react-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Integration of React Router with Effect for React applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"@effect/platform": "0.94.0",
|
|
26
26
|
"@effect/platform-node": "0.104.0",
|
|
27
|
-
"effect": "3.19.
|
|
27
|
+
"effect": "3.19.15",
|
|
28
28
|
"react-router": "7.9.6"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {},
|