@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 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,3 @@
1
+ export * from './lib/context.js';
2
+ export * from './lib/http-response.js';
3
+ export * as Runtime from './lib/runtime.js';
@@ -0,0 +1,3 @@
1
+ export * from './lib/context.js';
2
+ export * from './lib/http-response.js';
3
+ export * as Runtime from './lib/runtime.js';
@@ -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,5 @@
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
+ }
@@ -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,7 @@
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();
@@ -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
+ }