@effectify/react-remix 0.1.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 +102 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/lib/context.d.ts +9 -0
- package/dist/src/lib/context.js +5 -0
- package/dist/src/lib/http-response.d.ts +21 -0
- package/dist/src/lib/http-response.js +7 -0
- package/dist/src/lib/runtime.d.ts +21 -0
- package/dist/src/lib/runtime.js +49 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @effectify/react-remix
|
|
2
|
+
|
|
3
|
+
Integration of [Remix](https://remix.com/) with [Effect](https://effect.website/) for React applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# npm
|
|
9
|
+
npm install @effectify/react-remix
|
|
10
|
+
|
|
11
|
+
# yarn
|
|
12
|
+
yarn add @effectify/react-remix
|
|
13
|
+
|
|
14
|
+
# pnpm
|
|
15
|
+
pnpm add @effectify/react-remix
|
|
16
|
+
|
|
17
|
+
# bun
|
|
18
|
+
bun add @effectify/react-remix
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Basic Usage
|
|
22
|
+
|
|
23
|
+
### 1. Setup Server Runtime
|
|
24
|
+
|
|
25
|
+
Create a server runtime with your Effect layers:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// lib/server-runtime.ts
|
|
29
|
+
import { Runtime } from "@effectify/react-remix";
|
|
30
|
+
import * as Layer from "effect/Layer"
|
|
31
|
+
|
|
32
|
+
const layers = Layer.empty
|
|
33
|
+
|
|
34
|
+
export const { withLoaderEffect, withActionEffect } = Runtime.make(layers)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Use in Route Components
|
|
38
|
+
|
|
39
|
+
Use the Effect-based loaders and actions in your Remix routes:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// routes/home.tsx
|
|
43
|
+
import type * as Route from "./+types.home";
|
|
44
|
+
import { Ok, LoaderArgsContext } from "@effectify/react-remix";
|
|
45
|
+
import { withLoaderEffect } from "~/lib/server-runtime";
|
|
46
|
+
import * as Effect from "effect/Effect"
|
|
47
|
+
|
|
48
|
+
export const loader = withLoaderEffect(
|
|
49
|
+
Effect.gen(function* () {
|
|
50
|
+
const { request } = yield* LoaderArgsContext
|
|
51
|
+
yield* Effect.log("request", request)
|
|
52
|
+
return yield* Effect.succeed(new Ok({ data: { hello: 'world' }}))
|
|
53
|
+
})
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
export default function Home({ loaderData }: Route.ComponentProps) {
|
|
57
|
+
return (
|
|
58
|
+
<div>
|
|
59
|
+
<h1>Home</h1>
|
|
60
|
+
<pre>{JSON.stringify(loaderData.data, null, 2)}</pre>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## API
|
|
67
|
+
|
|
68
|
+
### `make(layers)`
|
|
69
|
+
|
|
70
|
+
Creates Effect-based runtime helpers for Remix.
|
|
71
|
+
|
|
72
|
+
#### Parameters
|
|
73
|
+
|
|
74
|
+
- `layers`: Effect Layer containing your application services and dependencies.
|
|
75
|
+
|
|
76
|
+
#### Returns
|
|
77
|
+
|
|
78
|
+
An object containing:
|
|
79
|
+
- `withLoaderEffect`: Wrapper for Remix loaders using Effect
|
|
80
|
+
- `withActionEffect`: Wrapper for Remix actions using Effect
|
|
81
|
+
|
|
82
|
+
### `LoaderArgsContext`
|
|
83
|
+
|
|
84
|
+
Effect context providing access to Remix loader arguments including:
|
|
85
|
+
- `request`: The incoming Request object
|
|
86
|
+
- `params`: Route parameters
|
|
87
|
+
- `context`: Additional context data
|
|
88
|
+
|
|
89
|
+
### Response Types
|
|
90
|
+
|
|
91
|
+
- `Ok(data)`: Successful response with data
|
|
92
|
+
- `Redirect(url)`: Redirect response
|
|
93
|
+
- `Error(message)`: Error response
|
|
94
|
+
|
|
95
|
+
## Requirements
|
|
96
|
+
|
|
97
|
+
- Remix 2+
|
|
98
|
+
- Effect ecosystem (`effect`)
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
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 {};
|
|
@@ -0,0 +1,21 @@
|
|
|
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 {};
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effectify/react-remix",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"@effectify/source": "./src/index.ts",
|
|
14
|
+
"types": "./dist/src/index.d.ts",
|
|
15
|
+
"import": "./dist/src/index.js",
|
|
16
|
+
"default": "./dist/src/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"!**/*.tsbuildinfo"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@effect/platform": "0.91.1",
|
|
25
|
+
"@effect/platform-node": "0.97.1",
|
|
26
|
+
"effect": "3.17.14",
|
|
27
|
+
"@remix-run/react": "2.17.1",
|
|
28
|
+
"@remix-run/node": "2.17.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {},
|
|
31
|
+
"peerDependencies": {},
|
|
32
|
+
"optionalDependencies": {}
|
|
33
|
+
}
|